From 661269e546346e5e01cc77056faa40cff9b574f8 Mon Sep 17 00:00:00 2001 From: sooshiance Date: Thu, 30 Jan 2025 10:16:16 +0330 Subject: [PATCH 1/4] feature/type-checker-compatible-with-mypy --- .gitignore | 1 + .pre-commit-config.yaml | 35 ++ json_log_formatter/__init__.py | 142 ++++---- mypy.ini | 14 + setup.py | 2 +- tests.py | 572 ++++++++++++++++++--------------- 6 files changed, 437 insertions(+), 329 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 mypy.ini diff --git a/.gitignore b/.gitignore index 2ea5a6c..8a6a5c2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __pycache__ /.tox /.cache /JSON_log_formatter.egg-info +.mypy_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a98251f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +repos: + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.9.0 + hooks: + - id: mypy + args: [--strict, --ignore-missing-imports] + additional_dependencies: [] + # If your project uses third-party packages, add them here: + # additional_dependencies: + # - types-requests + # - types-python-dateutil + + - repo: https://github.com/psf/black + rev: 24.3.0 + hooks: + - id: black + args: [--line-length=88] + + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + args: ["--profile", "black", "--filter-files"] + + - repo: https://github.com/PyCQA/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + additional_dependencies: [flake8-bugbear] + + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.1 + hooks: + - id: pyupgrade + args: [--py37-plus] diff --git a/json_log_formatter/__init__.py b/json_log_formatter/__init__.py index 6e0be3d..a03003f 100644 --- a/json_log_formatter/__init__.py +++ b/json_log_formatter/__init__.py @@ -1,33 +1,37 @@ +from __future__ import annotations # backward compatibility + import logging -from decimal import Decimal from datetime import datetime, timezone - +from decimal import Decimal import json - -BUILTIN_ATTRS = { - 'args', - 'asctime', - 'created', - 'exc_info', - 'exc_text', - 'filename', - 'funcName', - 'levelname', - 'levelno', - 'lineno', - 'module', - 'msecs', - 'message', - 'msg', - 'name', - 'pathname', - 'process', - 'processName', - 'relativeCreated', - 'stack_info', - 'taskName', - 'thread', - 'threadName', +from types import ModuleType +from typing import Any, Optional, Dict + + +BUILTIN_ATTRS: set[str] = { + "args", + "asctime", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "levelname", + "levelno", + "lineno", + "module", + "msecs", + "message", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack_info", + "taskName", + "thread", + "threadName", } @@ -58,13 +62,13 @@ class JSONFormatter(logging.Formatter): """ - json_lib = json + json_lib: ModuleType = json - def format(self, record): - message = record.getMessage() - extra = self.extra_from_record(record) - json_record = self.json_record(message, extra, record) - mutated_record = self.mutate_json_record(json_record) + def format(self, record: logging.LogRecord) -> Any | str: + message: str = record.getMessage() + extra: dict[str, Any] = self.extra_from_record(record) + json_record: dict[str, Any] = self.json_record(message, extra, record) + mutated_record: Optional[dict[str, Any]] = self.mutate_json_record(json_record) # Backwards compatibility: Functions that overwrite this but don't # return a new value will return None because they modified the # argument passed in. @@ -72,7 +76,7 @@ def format(self, record): mutated_record = json_record return self.to_json(mutated_record) - def to_json(self, record): + def to_json(self, record: dict[str, Any]) -> Any | str: """Converts record dict to a JSON string. It makes best effort to serialize a record (represents an object as a string) @@ -93,9 +97,9 @@ def to_json(self, record): try: return self.json_lib.dumps(record) except (TypeError, ValueError, OverflowError): - return '{}' + return "{}" - def extra_from_record(self, record): + def extra_from_record(self, record: logging.LogRecord) -> dict[str, Any]: """Returns `extra` dict you passed to logger. The `extra` keyword argument is used to populate the `__dict__` of @@ -108,7 +112,9 @@ def extra_from_record(self, record): if attr_name not in BUILTIN_ATTRS } - def json_record(self, message, extra, record): + def json_record( + self, message: str, extra: dict[str, Any], record: logging.LogRecord + ) -> dict[str, Any]: """Prepares a JSON payload which will be logged. Override this method to change JSON log format. @@ -120,16 +126,18 @@ def json_record(self, message, extra, record): :return: Dictionary which will be passed to JSON lib. """ - extra['message'] = message - if 'time' not in extra: - extra['time'] = datetime.now(timezone.utc) + extra["message"] = message + if "time" not in extra: + extra["time"] = datetime.now(timezone.utc) if record.exc_info: - extra['exc_info'] = self.formatException(record.exc_info) + extra["exc_info"] = self.formatException(record.exc_info) return extra - def mutate_json_record(self, json_record): + def mutate_json_record( + self, json_record: dict[str, Any] + ) -> Optional[Dict[str, Any]]: """Override it to convert fields of `json_record` to needed types. Default implementation converts `datetime` to string in ISO8601 format. @@ -142,7 +150,7 @@ def mutate_json_record(self, json_record): return json_record -def _json_serializable(obj): +def _json_serializable(obj: Any) -> Any: try: return obj.__dict__ except AttributeError: @@ -189,23 +197,29 @@ class VerboseJSONFormatter(JSONFormatter): https://docs.python.org/3/library/logging.html#logrecord-attributes. """ - def json_record(self, message, extra, record): - extra['filename'] = record.filename - extra['funcName'] = record.funcName - extra['levelname'] = record.levelname - extra['lineno'] = record.lineno - extra['module'] = record.module - extra['name'] = record.name - extra['pathname'] = record.pathname - extra['process'] = record.process - extra['processName'] = record.processName - if hasattr(record, 'stack_info'): - extra['stack_info'] = record.stack_info - else: - extra['stack_info'] = None - extra['thread'] = record.thread - extra['threadName'] = record.threadName - return super(VerboseJSONFormatter, self).json_record(message, extra, record) + + def json_record( + self, message: str, extra: dict[str, Any], record: logging.LogRecord + ) -> dict[str, Any]: + extra.update( + { + "filename": record.filename, + "funcName": record.funcName, + "levelname": record.levelname, + "lineno": record.lineno, + "module": record.module, + "name": record.name, + "pathname": record.pathname, + "process": record.process, + "processName": record.processName, + "stack_info": record.stack_info + if hasattr(record, "stack_info") + else None, + "thread": record.thread, + "threadName": record.threadName, + } + ) + return super().json_record(message, extra, record) class FlatJSONFormatter(JSONFormatter): @@ -230,10 +244,12 @@ class FlatJSONFormatter(JSONFormatter): """ - keep = (bool, int, float, Decimal, complex, str, datetime) + keep: tuple[type, ...] = (bool, int, float, Decimal, complex, str, datetime) - def json_record(self, message, extra, record): - extra = super(FlatJSONFormatter, self).json_record(message, extra, record) + def json_record( + self, message: str, extra: dict[str, Any], record: logging.LogRecord + ) -> dict[str, Any]: + extra = super().json_record(message, extra, record) return { k: v if v is None or isinstance(v, self.keep) else str(v) for k, v in extra.items() diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..74e95bb --- /dev/null +++ b/mypy.ini @@ -0,0 +1,14 @@ +[mypy] +python_version = 3.11 +# To recognize all the files +files = ['__init__.py', 'tests.py', 'setup.py' ] +strict = True +check_untyped_defs = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +no_implicit_optional = True +warn_redundant_casts = True +warn_unused_ignores = True +show_error_codes = True +show_column_numbers = True +ignore_missing_imports = False diff --git a/setup.py b/setup.py index 6cd6c89..6febf06 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup +from setuptools import setup # type: ignore setup( name='JSON-log-formatter', diff --git a/tests.py b/tests.py index b2012d7..3dfa6b8 100644 --- a/tests.py +++ b/tests.py @@ -2,6 +2,7 @@ from decimal import Decimal from io import BytesIO import unittest +from typing import Any import logging import json import os.path @@ -13,7 +14,7 @@ import simplejson try: - from cStringIO import StringIO + from cStringIO import StringIO # type: ignore except ImportError: from io import StringIO @@ -22,69 +23,73 @@ log_buffer = StringIO() json_handler = logging.StreamHandler(log_buffer) -logger = logging.getLogger('test') +logger = logging.getLogger("test") logger.addHandler(json_handler) logger.setLevel(logging.DEBUG) -logging.propagate = False +# This should be logger, not logging or use `type: ignore` instead +logger.propagate = False DATETIME = datetime(2015, 9, 1, 6, 9, 42, 797203) -DATETIME_ISO = u'2015-09-01T06:09:42.797203' +DATETIME_ISO = "2015-09-01T06:09:42.797203" settings.configure(DEBUG=True) class TestCase(unittest.TestCase): - def tearDown(self): + def tearDown(self) -> None: log_buffer.seek(0) log_buffer.truncate() class JSONFormatterTest(TestCase): - def setUp(self): + def setUp(self) -> None: json_handler.setFormatter(JSONFormatter()) - def test_given_time_is_used_in_log_record(self): - logger.info('Sign up', extra={'time': DATETIME}) + def test_given_time_is_used_in_log_record(self) -> None: + logger.info("Sign up", extra={"time": DATETIME}) expected_time = '"time": "2015-09-01T06:09:42.797203"' self.assertIn(expected_time, log_buffer.getvalue()) - def test_current_time_is_used_by_default_in_log_record(self): - logger.info('Sign up', extra={'fizz': 'bazz'}) + def test_current_time_is_used_by_default_in_log_record(self) -> None: + logger.info("Sign up", extra={"fizz": "bazz"}) self.assertNotIn(DATETIME_ISO, log_buffer.getvalue()) - def test_message_and_time_are_in_json_record_when_extra_is_blank(self): - logger.info('Sign up') + def test_message_and_time_are_in_json_record_when_extra_is_blank(self) -> None: + logger.info("Sign up") json_record = json.loads(log_buffer.getvalue()) - expected_fields = set([ - 'message', - 'time', - ]) + expected_fields = set( + [ + "message", + "time", + ] + ) self.assertTrue(expected_fields.issubset(json_record)) - def test_message_and_time_and_extra_are_in_json_record_when_extra_is_provided(self): - logger.info('Sign up', extra={'fizz': 'bazz'}) + def test_message_and_time_and_extra_are_in_json_record_when_extra_is_provided( + self, + ) -> None: + logger.info("Sign up", extra={"fizz": "bazz"}) json_record = json.loads(log_buffer.getvalue()) - expected_fields = set([ - 'message', - 'time', - 'fizz', - ]) + expected_fields = set( + [ + "message", + "time", + "fizz", + ] + ) self.assertTrue(expected_fields.issubset(json_record)) - def test_exc_info_is_logged(self): + def test_exc_info_is_logged(self) -> None: try: - raise ValueError('something wrong') + raise ValueError("something wrong") except ValueError: - logger.error('Request failed', exc_info=True) + logger.error("Request failed", exc_info=True) json_record = json.loads(log_buffer.getvalue()) - self.assertIn( - 'Traceback (most recent call last)', - json_record['exc_info'] - ) + self.assertIn("Traceback (most recent call last)", json_record["exc_info"]) class MutatingFormatter(JSONFormatter): - def mutate_json_record(self, json_record): + def mutate_json_record(self, json_record: dict[str, Any]) -> dict[str, Any]: new_record = {} for k, v in json_record.items(): if isinstance(v, datetime): @@ -94,338 +99,375 @@ def mutate_json_record(self, json_record): class MutatingFormatterTest(TestCase): - def setUp(self): + def setUp(self) -> None: json_handler.setFormatter(MutatingFormatter()) - def test_new_record_accepted(self): - logger.info('Sign up', extra={'fizz': DATETIME}) + def test_new_record_accepted(self) -> None: + logger.info("Sign up", extra={"fizz": DATETIME}) json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['fizz'], DATETIME_ISO) + self.assertEqual(json_record["fizz"], DATETIME_ISO) class JsonLibTest(TestCase): - def setUp(self): + def setUp(self) -> None: json_handler.setFormatter(JSONFormatter()) - def test_builtin_types_are_serialized(self): - logger.log(level=logging.ERROR, msg='Payment was sent', extra={ - 'first_name': 'bob', - 'amount': 0.00497265, - 'context': { - 'tags': ['fizz', 'bazz'], + def test_builtin_types_are_serialized(self) -> None: + logger.log( + level=logging.ERROR, + msg="Payment was sent", + extra={ + "first_name": "bob", + "amount": 0.00497265, + "context": { + "tags": ["fizz", "bazz"], + }, + "things": ("a", "b"), + "ok": True, + "none": None, }, - 'things': ('a', 'b'), - 'ok': True, - 'none': None, - }) + ) json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['first_name'], 'bob') - self.assertEqual(json_record['amount'], 0.00497265) - self.assertEqual(json_record['context'], {'tags': ['fizz', 'bazz']}) - self.assertEqual(json_record['things'], ['a', 'b']) - self.assertEqual(json_record['ok'], True) - self.assertEqual(json_record['none'], None) - - def test_decimal_is_serialized_as_string(self): - logger.log(level=logging.ERROR, msg='Payment was sent', extra={ - 'amount': Decimal('0.00497265') - }) + self.assertEqual(json_record["first_name"], "bob") + self.assertEqual(json_record["amount"], 0.00497265) + self.assertEqual(json_record["context"], {"tags": ["fizz", "bazz"]}) + self.assertEqual(json_record["things"], ["a", "b"]) + self.assertEqual(json_record["ok"], True) + self.assertEqual(json_record["none"], None) + + def test_decimal_is_serialized_as_string(self) -> None: + logger.log( + level=logging.ERROR, + msg="Payment was sent", + extra={"amount": Decimal("0.00497265")}, + ) expected_amount = '"amount": "0.00497265"' self.assertIn(expected_amount, log_buffer.getvalue()) - def test_django_wsgi_request_is_serialized_as_dict(self): - request = WSGIRequest({ - 'PATH_INFO': 'bogus', - 'REQUEST_METHOD': 'bogus', - 'CONTENT_TYPE': 'text/html; charset=utf8', - 'wsgi.input': BytesIO(b''), - }) - - logger.log(level=logging.ERROR, msg='Django response error', extra={ - 'status_code': 500, - 'request': request - }) + def test_django_wsgi_request_is_serialized_as_dict(self) -> None: + request = WSGIRequest( + { + "PATH_INFO": "bogus", + "REQUEST_METHOD": "bogus", + "CONTENT_TYPE": "text/html; charset=utf8", + "wsgi.input": BytesIO(b""), + } + ) + + logger.log( + level=logging.ERROR, + msg="Django response error", + extra={"status_code": 500, "request": request}, + ) json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['status_code'], 500) - self.assertEqual(json_record['request']['path'], '/bogus') - self.assertEqual(json_record['request']['method'], 'BOGUS') + self.assertEqual(json_record["status_code"], 500) + self.assertEqual(json_record["request"]["path"], "/bogus") + self.assertEqual(json_record["request"]["method"], "BOGUS") - def test_json_circular_reference_is_handled(self): - d = {} - d['circle'] = d - logger.info('Referer checking', extra=d) - self.assertEqual('{}\n', log_buffer.getvalue()) + def test_json_circular_reference_is_handled(self) -> None: + d: dict[str, object] = {} + d["circle"] = d + logger.info("Referer checking", extra=d) + self.assertEqual("{}\n", log_buffer.getvalue()) class UjsonLibTest(TestCase): - def setUp(self): + def setUp(self) -> None: formatter = JSONFormatter() formatter.json_lib = ujson json_handler.setFormatter(formatter) - def test_builtin_types_are_serialized(self): - logger.log(level=logging.ERROR, msg='Payment was sent', extra={ - 'first_name': 'bob', - 'amount': 0.00497265, - 'context': { - 'tags': ['fizz', 'bazz'], + def test_builtin_types_are_serialized(self) -> None: + logger.log( + level=logging.ERROR, + msg="Payment was sent", + extra={ + "first_name": "bob", + "amount": 0.00497265, + "context": { + "tags": ["fizz", "bazz"], + }, + "things": ("a", "b"), + "ok": True, + "none": None, }, - 'things': ('a', 'b'), - 'ok': True, - 'none': None, - }) + ) json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['first_name'], 'bob') - self.assertEqual(json_record['amount'], 0.00497265) - self.assertEqual(json_record['context'], {'tags': ['fizz', 'bazz']}) - self.assertEqual(json_record['things'], ['a', 'b']) - self.assertEqual(json_record['ok'], True) - self.assertEqual(json_record['none'], None) - - def test_decimal_is_serialized_as_number(self): - logger.info('Payment was sent', extra={ - 'amount': Decimal('0.00497265') - }) + self.assertEqual(json_record["first_name"], "bob") + self.assertEqual(json_record["amount"], 0.00497265) + self.assertEqual(json_record["context"], {"tags": ["fizz", "bazz"]}) + self.assertEqual(json_record["things"], ["a", "b"]) + self.assertEqual(json_record["ok"], True) + self.assertEqual(json_record["none"], None) + + def test_decimal_is_serialized_as_number(self) -> None: + logger.info("Payment was sent", extra={"amount": Decimal("0.00497265")}) expected_amount = '"amount":0.00497265' self.assertIn(expected_amount, log_buffer.getvalue()) - def test_zero_expected_when_decimal_is_in_scientific_notation(self): - logger.info('Payment was sent', extra={ - 'amount': Decimal('0E-8') - }) + def test_zero_expected_when_decimal_is_in_scientific_notation(self) -> None: + logger.info("Payment was sent", extra={"amount": Decimal("0E-8")}) expected_amount = '"amount":0.0' self.assertIn(expected_amount, log_buffer.getvalue()) - def test_django_wsgi_request_is_serialized_as_empty_list(self): - request = WSGIRequest({ - 'PATH_INFO': 'bogus', - 'REQUEST_METHOD': 'bogus', - 'CONTENT_TYPE': 'text/html; charset=utf8', - 'wsgi.input': BytesIO(b''), - }) - - logger.log(level=logging.ERROR, msg='Django response error', extra={ - 'status_code': 500, - 'request': request - }) + def test_django_wsgi_request_is_serialized_as_empty_list(self) -> None: + request = WSGIRequest( + { + "PATH_INFO": "bogus", + "REQUEST_METHOD": "bogus", + "CONTENT_TYPE": "text/html; charset=utf8", + "wsgi.input": BytesIO(b""), + } + ) + + logger.log( + level=logging.ERROR, + msg="Django response error", + extra={"status_code": 500, "request": request}, + ) json_record = json.loads(log_buffer.getvalue()) - if 'status_code' in json_record: - self.assertEqual(json_record['status_code'], 500) - if 'request' in json_record: - self.assertEqual(json_record['request']['path'], '/bogus') - self.assertEqual(json_record['request']['method'], 'BOGUS') + if "status_code" in json_record: + self.assertEqual(json_record["status_code"], 500) + if "request" in json_record: + self.assertEqual(json_record["request"]["path"], "/bogus") + self.assertEqual(json_record["request"]["method"], "BOGUS") - def test_json_circular_reference_is_handled(self): - d = {} - d['circle'] = d - logger.info('Referer checking', extra=d) - self.assertEqual('{}\n', log_buffer.getvalue()) + def test_json_circular_reference_is_handled(self) -> None: + d: dict[str, object] = {} + d["circle"] = d + logger.info("Referer checking", extra=d) + self.assertEqual("{}\n", log_buffer.getvalue()) class SimplejsonLibTest(TestCase): - def setUp(self): + def setUp(self) -> None: formatter = JSONFormatter() formatter.json_lib = simplejson json_handler.setFormatter(formatter) - def test_builtin_types_are_serialized(self): - logger.log(level=logging.ERROR, msg='Payment was sent', extra={ - 'first_name': 'bob', - 'amount': 0.00497265, - 'context': { - 'tags': ['fizz', 'bazz'], + def test_builtin_types_are_serialized(self) -> None: + logger.log( + level=logging.ERROR, + msg="Payment was sent", + extra={ + "first_name": "bob", + "amount": 0.00497265, + "context": { + "tags": ["fizz", "bazz"], + }, + "things": ("a", "b"), + "ok": True, + "none": None, }, - 'things': ('a', 'b'), - 'ok': True, - 'none': None, - }) + ) json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['first_name'], 'bob') - self.assertEqual(json_record['amount'], 0.00497265) - self.assertEqual(json_record['context'], {'tags': ['fizz', 'bazz']}) - self.assertEqual(json_record['things'], ['a', 'b']) - self.assertEqual(json_record['ok'], True) - self.assertEqual(json_record['none'], None) - - def test_decimal_is_serialized_as_number(self): - logger.info('Payment was sent', extra={ - 'amount': Decimal('0.00497265') - }) + self.assertEqual(json_record["first_name"], "bob") + self.assertEqual(json_record["amount"], 0.00497265) + self.assertEqual(json_record["context"], {"tags": ["fizz", "bazz"]}) + self.assertEqual(json_record["things"], ["a", "b"]) + self.assertEqual(json_record["ok"], True) + self.assertEqual(json_record["none"], None) + + def test_decimal_is_serialized_as_number(self) -> None: + logger.info("Payment was sent", extra={"amount": Decimal("0.00497265")}) expected_amount = '"amount": 0.00497265' self.assertIn(expected_amount, log_buffer.getvalue()) - def test_decimal_is_serialized_as_it_is_when_it_is_in_scientific_notation(self): - logger.info('Payment was sent', extra={ - 'amount': Decimal('0E-8') - }) + def test_decimal_is_serialized_as_it_is_when_it_is_in_scientific_notation( + self, + ) -> None: + logger.info("Payment was sent", extra={"amount": Decimal("0E-8")}) expected_amount = '"amount": 0E-8' self.assertIn(expected_amount, log_buffer.getvalue()) - def test_django_wsgi_request_is_serialized_as_dict(self): - request = WSGIRequest({ - 'PATH_INFO': 'bogus', - 'REQUEST_METHOD': 'bogus', - 'CONTENT_TYPE': 'text/html; charset=utf8', - 'wsgi.input': BytesIO(b''), - }) - - logger.log(level=logging.ERROR, msg='Django response error', extra={ - 'status_code': 500, - 'request': request - }) + def test_django_wsgi_request_is_serialized_as_dict(self) -> None: + request = WSGIRequest( + { + "PATH_INFO": "bogus", + "REQUEST_METHOD": "bogus", + "CONTENT_TYPE": "text/html; charset=utf8", + "wsgi.input": BytesIO(b""), + } + ) + + logger.log( + level=logging.ERROR, + msg="Django response error", + extra={"status_code": 500, "request": request}, + ) json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['status_code'], 500) - self.assertEqual(json_record['request']['path'], '/bogus') - self.assertEqual(json_record['request']['method'], 'BOGUS') + self.assertEqual(json_record["status_code"], 500) + self.assertEqual(json_record["request"]["path"], "/bogus") + self.assertEqual(json_record["request"]["method"], "BOGUS") - def test_json_circular_reference_is_handled(self): - d = {} - d['circle'] = d - logger.info('Referer checking', extra=d) - self.assertEqual('{}\n', log_buffer.getvalue()) + def test_json_circular_reference_is_handled(self) -> None: + d: dict[str, object] = {} + d["circle"] = d + logger.info("Referer checking", extra=d) + self.assertEqual("{}\n", log_buffer.getvalue()) class VerboseJSONFormatterTest(TestCase): - def setUp(self): + def setUp(self) -> None: json_handler.setFormatter(VerboseJSONFormatter()) - def test_file_name_is_testspy(self): - logger.error('An error has occured') + def test_file_name_is_testspy(self) -> None: + logger.error("An error has occured") json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['filename'], 'tests.py') + self.assertEqual(json_record["filename"], "tests.py") - def test_function_name(self): - logger.error('An error has occured') + def test_function_name(self) -> None: + logger.error("An error has occured") json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['funcName'], 'test_function_name') + self.assertEqual(json_record["funcName"], "test_function_name") - def test_level_name_is_error(self): - logger.error('An error has occured') + def test_level_name_is_error(self) -> None: + logger.error("An error has occured") json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['levelname'], 'ERROR') + self.assertEqual(json_record["levelname"], "ERROR") - def test_module_name_is_tests(self): - logger.error('An error has occured') + def test_module_name_is_tests(self) -> None: + logger.error("An error has occured") json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['module'], 'tests') + self.assertEqual(json_record["module"], "tests") - def test_logger_name_is_test(self): - logger.error('An error has occured') + def test_logger_name_is_test(self) -> None: + logger.error("An error has occured") json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['name'], 'test') + self.assertEqual(json_record["name"], "test") - def test_path_name_is_test(self): - logger.error('An error has occured') + def test_path_name_is_test(self) -> None: + logger.error("An error has occured") json_record = json.loads(log_buffer.getvalue()) - self.assertIn(os.path.basename(os.path.abspath('.')) + '/tests.py', json_record['pathname']) + self.assertIn( + os.path.basename(os.path.abspath(".")) + "/tests.py", + json_record["pathname"], + ) - def test_process_name_is_MainProcess(self): - logger.error('An error has occured') + def test_process_name_is_MainProcess(self) -> None: + logger.error("An error has occured") json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['processName'], 'MainProcess') + self.assertEqual(json_record["processName"], "MainProcess") - def test_thread_name_is_MainThread(self): - logger.error('An error has occured') + def test_thread_name_is_MainThread(self) -> None: + logger.error("An error has occured") json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['threadName'], 'MainThread') + self.assertEqual(json_record["threadName"], "MainThread") - def test_stack_info_is_none(self): - logger.error('An error has occured') + def test_stack_info_is_none(self) -> None: + logger.error("An error has occured") json_record = json.loads(log_buffer.getvalue()) - self.assertIsNone(json_record['stack_info']) + self.assertIsNone(json_record["stack_info"]) class FlatJSONFormatterTest(TestCase): - def setUp(self): + def setUp(self) -> None: json_handler.setFormatter(FlatJSONFormatter()) - def test_given_time_is_used_in_log_record(self): - logger.info('Sign up', extra={'time': DATETIME}) + def test_given_time_is_used_in_log_record(self) -> None: + logger.info("Sign up", extra={"time": DATETIME}) expected_time = '"time": "2015-09-01T06:09:42.797203"' self.assertIn(expected_time, log_buffer.getvalue()) - def test_current_time_is_used_by_default_in_log_record(self): - logger.info('Sign up', extra={'fizz': 'bazz'}) + def test_current_time_is_used_by_default_in_log_record(self) -> None: + logger.info("Sign up", extra={"fizz": "bazz"}) self.assertNotIn(DATETIME_ISO, log_buffer.getvalue()) - def test_message_and_time_are_in_json_record_when_extra_is_blank(self): - logger.info('Sign up') + def test_message_and_time_are_in_json_record_when_extra_is_blank(self) -> None: + logger.info("Sign up") json_record = json.loads(log_buffer.getvalue()) - expected_fields = set([ - 'message', - 'time', - ]) + expected_fields = set( + [ + "message", + "time", + ] + ) self.assertTrue(expected_fields.issubset(json_record)) - def test_message_and_time_and_extra_are_in_json_record_when_extra_is_provided(self): - logger.info('Sign up', extra={'fizz': 'bazz'}) + def test_message_and_time_and_extra_are_in_json_record_when_extra_is_provided( + self, + ) -> None: + logger.info("Sign up", extra={"fizz": "bazz"}) json_record = json.loads(log_buffer.getvalue()) - expected_fields = set([ - 'message', - 'time', - 'fizz', - ]) + expected_fields = set( + [ + "message", + "time", + "fizz", + ] + ) self.assertTrue(expected_fields.issubset(json_record)) - def test_exc_info_is_logged(self): + def test_exc_info_is_logged(self) -> None: try: - raise ValueError('something wrong') + raise ValueError("something wrong") except ValueError: - logger.error('Request failed', exc_info=True) + logger.error("Request failed", exc_info=True) json_record = json.loads(log_buffer.getvalue()) - self.assertIn( - 'Traceback (most recent call last)', - json_record['exc_info'] - ) - - def test_builtin_types_are_serialized(self): - logger.log(level=logging.ERROR, msg='Payment was sent', extra={ - 'first_name': 'bob', - 'amount': 0.00497265, - 'context': { - 'tags': ['fizz', 'bazz'], + self.assertIn("Traceback (most recent call last)", json_record["exc_info"]) + + def test_builtin_types_are_serialized(self) -> None: + logger.log( + level=logging.ERROR, + msg="Payment was sent", + extra={ + "first_name": "bob", + "amount": 0.00497265, + "context": { + "tags": ["fizz", "bazz"], + }, + "things": ("a", "b"), + "ok": True, + "none": None, }, - 'things': ('a', 'b'), - 'ok': True, - 'none': None, - }) + ) json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['first_name'], 'bob') - self.assertEqual(json_record['amount'], 0.00497265) - self.assertEqual(json_record['context'], "{'tags': ['fizz', 'bazz']}") - self.assertEqual(json_record['things'], "('a', 'b')") - self.assertEqual(json_record['ok'], True) - self.assertEqual(json_record['none'], None) - - def test_decimal_is_serialized_as_string(self): - logger.log(level=logging.ERROR, msg='Payment was sent', extra={ - 'amount': Decimal('0.00497265') - }) + self.assertEqual(json_record["first_name"], "bob") + self.assertEqual(json_record["amount"], 0.00497265) + self.assertEqual(json_record["context"], "{'tags': ['fizz', 'bazz']}") + self.assertEqual(json_record["things"], "('a', 'b')") + self.assertEqual(json_record["ok"], True) + self.assertEqual(json_record["none"], None) + + def test_decimal_is_serialized_as_string(self) -> None: + logger.log( + level=logging.ERROR, + msg="Payment was sent", + extra={"amount": Decimal("0.00497265")}, + ) expected_amount = '"amount": "0.00497265"' self.assertIn(expected_amount, log_buffer.getvalue()) - def test_django_wsgi_request_is_serialized_as_dict(self): - request = WSGIRequest({ - 'PATH_INFO': 'bogus', - 'REQUEST_METHOD': 'bogus', - 'CONTENT_TYPE': 'text/html; charset=utf8', - 'wsgi.input': BytesIO(b''), - }) - - logger.log(level=logging.ERROR, msg='Django response error', extra={ - 'status_code': 500, - 'request': request, - 'dict': { - 'request': request, + def test_django_wsgi_request_is_serialized_as_dict(self) -> None: + request = WSGIRequest( + { + "PATH_INFO": "bogus", + "REQUEST_METHOD": "bogus", + "CONTENT_TYPE": "text/html; charset=utf8", + "wsgi.input": BytesIO(b""), + } + ) + + logger.log( + level=logging.ERROR, + msg="Django response error", + extra={ + "status_code": 500, + "request": request, + "dict": { + "request": request, + }, + "list": [request], }, - 'list': [request], - }) + ) json_record = json.loads(log_buffer.getvalue()) - self.assertEqual(json_record['status_code'], 500) - self.assertEqual(json_record['request'], "") - self.assertEqual(json_record['dict'], "{'request': }") - self.assertEqual(json_record['list'], "[]") + self.assertEqual(json_record["status_code"], 500) + self.assertEqual(json_record["request"], "") + self.assertEqual( + json_record["dict"], "{'request': }" + ) + self.assertEqual(json_record["list"], "[]") From 5d0e465d386c15ecdfbd98d30fbebe018a852f91 Mon Sep 17 00:00:00 2001 From: sooshiance Date: Fri, 28 Feb 2025 10:32:44 +0330 Subject: [PATCH 2/4] remove backward compatibility --- json_log_formatter/__init__.py | 9 +++------ tests.py | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/json_log_formatter/__init__.py b/json_log_formatter/__init__.py index a03003f..a8b6979 100644 --- a/json_log_formatter/__init__.py +++ b/json_log_formatter/__init__.py @@ -1,11 +1,10 @@ -from __future__ import annotations # backward compatibility +from typing import Any import logging from datetime import datetime, timezone from decimal import Decimal import json from types import ModuleType -from typing import Any, Optional, Dict BUILTIN_ATTRS: set[str] = { @@ -68,7 +67,7 @@ def format(self, record: logging.LogRecord) -> Any | str: message: str = record.getMessage() extra: dict[str, Any] = self.extra_from_record(record) json_record: dict[str, Any] = self.json_record(message, extra, record) - mutated_record: Optional[dict[str, Any]] = self.mutate_json_record(json_record) + mutated_record: dict[str, Any] = self.mutate_json_record(json_record) # Backwards compatibility: Functions that overwrite this but don't # return a new value will return None because they modified the # argument passed in. @@ -135,9 +134,7 @@ def json_record( return extra - def mutate_json_record( - self, json_record: dict[str, Any] - ) -> Optional[Dict[str, Any]]: + def mutate_json_record(self, json_record: dict[str, Any]) -> dict[str, Any]: """Override it to convert fields of `json_record` to needed types. Default implementation converts `datetime` to string in ISO8601 format. diff --git a/tests.py b/tests.py index 3dfa6b8..60b9cc1 100644 --- a/tests.py +++ b/tests.py @@ -10,8 +10,8 @@ from django.core.handlers.wsgi import WSGIRequest from django.conf import settings -import ujson -import simplejson +import ujson # type: ignore +import simplejson # type: ignore try: from cStringIO import StringIO # type: ignore From 6a6d39999298e53d0c384f8ea354473e3e083dfe Mon Sep 17 00:00:00 2001 From: sooshiance Date: Wed, 4 Jun 2025 08:57:01 +0330 Subject: [PATCH 3/4] feat: remove pre commit hooks and make few changes --- .pre-commit-config.yaml | 35 ----------------------------------- mypy.ini | 1 - pyproject.toml | 3 +++ setup.py | 30 +++++++++++++++--------------- tests.py | 1 - 5 files changed, 18 insertions(+), 52 deletions(-) delete mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index a98251f..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,35 +0,0 @@ -repos: - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.9.0 - hooks: - - id: mypy - args: [--strict, --ignore-missing-imports] - additional_dependencies: [] - # If your project uses third-party packages, add them here: - # additional_dependencies: - # - types-requests - # - types-python-dateutil - - - repo: https://github.com/psf/black - rev: 24.3.0 - hooks: - - id: black - args: [--line-length=88] - - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - args: ["--profile", "black", "--filter-files"] - - - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 - hooks: - - id: flake8 - additional_dependencies: [flake8-bugbear] - - - repo: https://github.com/asottile/pyupgrade - rev: v3.15.1 - hooks: - - id: pyupgrade - args: [--py37-plus] diff --git a/mypy.ini b/mypy.ini index 74e95bb..b22b34a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,4 @@ [mypy] -python_version = 3.11 # To recognize all the files files = ['__init__.py', 'tests.py', 'setup.py' ] strict = True diff --git a/pyproject.toml b/pyproject.toml index cfb46b2..ac9fd07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,3 +26,6 @@ classifiers=[ [project.urls] repository = "https://github.com/marselester/json-log-formatter" + +[tool.mypy] +warn_unused_configs = true diff --git a/setup.py b/setup.py index 6febf06..52d7367 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,20 @@ -from setuptools import setup # type: ignore +from setuptools import setup # type: ignore setup( - name='JSON-log-formatter', - version='1.1', - license='MIT', - packages=['json_log_formatter'], - author='Marsel Mavletkulov', - url='https://github.com/marselester/json-log-formatter', - description='JSON log formatter', - long_description=open('README.rst').read(), + name="JSON-log-formatter", + version="1.1", + license="MIT", + packages=["json_log_formatter"], + author="Marsel Mavletkulov", + url="https://github.com/marselester/json-log-formatter", + description="JSON log formatter", + long_description=open("README.rst").read(), classifiers=[ - 'License :: OSI Approved :: MIT License', - 'Intended Audience :: Developers', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Topic :: Software Development :: Libraries :: Python Modules' + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Libraries :: Python Modules", ], ) diff --git a/tests.py b/tests.py index 60b9cc1..75bf3ba 100644 --- a/tests.py +++ b/tests.py @@ -26,7 +26,6 @@ logger = logging.getLogger("test") logger.addHandler(json_handler) logger.setLevel(logging.DEBUG) -# This should be logger, not logging or use `type: ignore` instead logger.propagate = False DATETIME = datetime(2015, 9, 1, 6, 9, 42, 797203) From 4a4920ed8cba934c97f31c0f8ea9d8a0f083bc56 Mon Sep 17 00:00:00 2001 From: sooshiance Date: Wed, 4 Jun 2025 09:19:51 +0330 Subject: [PATCH 4/4] fix: Resolve merge conflicts --- setup.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/setup.py b/setup.py index 52d7367..6febf06 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,20 @@ -from setuptools import setup # type: ignore +from setuptools import setup # type: ignore setup( - name="JSON-log-formatter", - version="1.1", - license="MIT", - packages=["json_log_formatter"], - author="Marsel Mavletkulov", - url="https://github.com/marselester/json-log-formatter", - description="JSON log formatter", - long_description=open("README.rst").read(), + name='JSON-log-formatter', + version='1.1', + license='MIT', + packages=['json_log_formatter'], + author='Marsel Mavletkulov', + url='https://github.com/marselester/json-log-formatter', + description='JSON log formatter', + long_description=open('README.rst').read(), classifiers=[ - "License :: OSI Approved :: MIT License", - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Topic :: Software Development :: Libraries :: Python Modules", + 'License :: OSI Approved :: MIT License', + 'Intended Audience :: Developers', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Topic :: Software Development :: Libraries :: Python Modules' ], )