python-json-logger 4.0.0rc1__tar.gz → 4.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {python_json_logger-4.0.0rc1/src/python_json_logger.egg-info → python_json_logger-4.2.0}/PKG-INFO +4 -28
  2. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/pyproject.toml +8 -12
  3. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0/src/python_json_logger.egg-info}/PKG-INFO +4 -28
  4. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/src/python_json_logger.egg-info/SOURCES.txt +0 -1
  5. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/src/pythonjsonlogger/core.py +47 -42
  6. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/src/pythonjsonlogger/defaults.py +1 -7
  7. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/src/pythonjsonlogger/json.py +5 -4
  8. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/src/pythonjsonlogger/msgspec.py +3 -2
  9. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/src/pythonjsonlogger/orjson.py +4 -3
  10. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/tests/test_dictconfig.py +3 -2
  11. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/tests/test_formatters.py +180 -7
  12. python_json_logger-4.0.0rc1/src/python_json_logger.egg-info/requires.txt +0 -28
  13. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/LICENSE +0 -0
  14. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/MANIFEST.in +0 -0
  15. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/NOTICE +0 -0
  16. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/README.md +0 -0
  17. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/setup.cfg +0 -0
  18. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/src/python_json_logger.egg-info/dependency_links.txt +0 -0
  19. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/src/python_json_logger.egg-info/top_level.txt +0 -0
  20. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/src/pythonjsonlogger/__init__.py +0 -0
  21. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/src/pythonjsonlogger/exception.py +0 -0
  22. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/src/pythonjsonlogger/jsonlogger.py +0 -0
  23. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/src/pythonjsonlogger/py.typed +0 -0
  24. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/src/pythonjsonlogger/utils.py +0 -0
  25. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/tests/__init__.py +0 -0
  26. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/tests/test_deprecation.py +0 -0
  27. {python_json_logger-4.0.0rc1 → python_json_logger-4.2.0}/tests/test_missing.py +0 -0
@@ -1,50 +1,26 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-json-logger
3
- Version: 4.0.0rc1
3
+ Version: 4.2.0
4
4
  Summary: JSON Log Formatter for the Python Logging Package
5
5
  Author-email: Zakaria Zajac <zak@madzak.com>, Nicholas Hairs <info+python-json-logger@nicholashairs.com>
6
6
  Maintainer-email: Nicholas Hairs <info+python-json-logger@nicholashairs.com>
7
- License: BSD-2-Clause License
7
+ License-Expression: BSD-2-Clause
8
8
  Project-URL: Homepage, https://nhairs.github.io/python-json-logger
9
9
  Project-URL: GitHub, https://github.com/nhairs/python-json-logger
10
10
  Classifier: Development Status :: 6 - Mature
11
11
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: BSD License
13
12
  Classifier: Operating System :: OS Independent
14
13
  Classifier: Programming Language :: Python :: 3 :: Only
15
- Classifier: Programming Language :: Python :: 3.8
16
- Classifier: Programming Language :: Python :: 3.9
17
14
  Classifier: Programming Language :: Python :: 3.10
18
15
  Classifier: Programming Language :: Python :: 3.11
19
16
  Classifier: Programming Language :: Python :: 3.12
20
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
21
19
  Classifier: Topic :: System :: Logging
22
20
  Classifier: Typing :: Typed
23
- Requires-Python: >=3.8
21
+ Requires-Python: >=3.10
24
22
  Description-Content-Type: text/markdown
25
23
  License-File: LICENSE
26
- License-File: NOTICE
27
- Requires-Dist: typing_extensions; python_version < "3.10"
28
- Provides-Extra: dev
29
- Requires-Dist: orjson; implementation_name != "pypy" and extra == "dev"
30
- Requires-Dist: msgspec; implementation_name != "pypy" and extra == "dev"
31
- Requires-Dist: validate-pyproject[all]; extra == "dev"
32
- Requires-Dist: black; extra == "dev"
33
- Requires-Dist: pylint; extra == "dev"
34
- Requires-Dist: mypy; extra == "dev"
35
- Requires-Dist: pytest; extra == "dev"
36
- Requires-Dist: freezegun; extra == "dev"
37
- Requires-Dist: backports.zoneinfo; python_version < "3.9" and extra == "dev"
38
- Requires-Dist: tzdata; extra == "dev"
39
- Requires-Dist: build; extra == "dev"
40
- Requires-Dist: mkdocs; extra == "dev"
41
- Requires-Dist: mkdocs-material>=8.5; extra == "dev"
42
- Requires-Dist: mkdocs-awesome-pages-plugin; extra == "dev"
43
- Requires-Dist: mdx_truly_sane_lists; extra == "dev"
44
- Requires-Dist: mkdocstrings[python]; extra == "dev"
45
- Requires-Dist: mkdocs-gen-files; extra == "dev"
46
- Requires-Dist: mkdocs-literate-nav; extra == "dev"
47
- Requires-Dist: mike; extra == "dev"
48
24
  Dynamic: license-file
49
25
 
50
26
  [![PyPi](https://img.shields.io/pypi/v/python-json-logger.svg)](https://pypi.python.org/pypi/python-json-logger/)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-json-logger"
7
- version = "4.0.0.rc1"
7
+ version = "4.2.0"
8
8
  description = "JSON Log Formatter for the Python Logging Package"
9
9
  authors = [
10
10
  {name = "Zakaria Zajac", email = "zak@madzak.com"},
@@ -15,26 +15,22 @@ maintainers = [
15
15
  ]
16
16
 
17
17
  # Dependency Information
18
- requires-python = ">=3.8"
19
- dependencies = [
20
- "typing_extensions;python_version<'3.10'",
21
- ]
18
+ requires-python = ">=3.10"
22
19
 
23
20
  # Extra information
24
21
  readme = "README.md"
25
- license = {text = "BSD-2-Clause License"}
22
+ license = "BSD-2-Clause"
23
+ license-files = ["LICENSE"]
26
24
  classifiers = [
27
25
  "Development Status :: 6 - Mature",
28
26
  "Intended Audience :: Developers",
29
- "License :: OSI Approved :: BSD License",
30
27
  "Operating System :: OS Independent",
31
28
  "Programming Language :: Python :: 3 :: Only",
32
- "Programming Language :: Python :: 3.8",
33
- "Programming Language :: Python :: 3.9",
34
29
  "Programming Language :: Python :: 3.10",
35
30
  "Programming Language :: Python :: 3.11",
36
31
  "Programming Language :: Python :: 3.12",
37
32
  "Programming Language :: Python :: 3.13",
33
+ "Programming Language :: Python :: 3.14",
38
34
  "Topic :: System :: Logging",
39
35
  "Typing :: Typed",
40
36
  ]
@@ -43,7 +39,7 @@ classifiers = [
43
39
  Homepage = "https://nhairs.github.io/python-json-logger"
44
40
  GitHub = "https://github.com/nhairs/python-json-logger"
45
41
 
46
- [project.optional-dependencies]
42
+ [dependency-groups]
47
43
  dev = [
48
44
  ## Optional but required for dev
49
45
  "orjson;implementation_name!='pypy'",
@@ -52,11 +48,11 @@ dev = [
52
48
  "validate-pyproject[all]",
53
49
  "black",
54
50
  "pylint",
55
- "mypy",
51
+ "mypy<2; implementation_name == 'pypy' and python_version < '3.11'",
52
+ "mypy; implementation_name != 'pypy' or python_version >= '3.11'",
56
53
  ## Test
57
54
  "pytest",
58
55
  "freezegun",
59
- "backports.zoneinfo;python_version<'3.9'",
60
56
  "tzdata",
61
57
  ## Build
62
58
  "build",
@@ -1,50 +1,26 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-json-logger
3
- Version: 4.0.0rc1
3
+ Version: 4.2.0
4
4
  Summary: JSON Log Formatter for the Python Logging Package
5
5
  Author-email: Zakaria Zajac <zak@madzak.com>, Nicholas Hairs <info+python-json-logger@nicholashairs.com>
6
6
  Maintainer-email: Nicholas Hairs <info+python-json-logger@nicholashairs.com>
7
- License: BSD-2-Clause License
7
+ License-Expression: BSD-2-Clause
8
8
  Project-URL: Homepage, https://nhairs.github.io/python-json-logger
9
9
  Project-URL: GitHub, https://github.com/nhairs/python-json-logger
10
10
  Classifier: Development Status :: 6 - Mature
11
11
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: BSD License
13
12
  Classifier: Operating System :: OS Independent
14
13
  Classifier: Programming Language :: Python :: 3 :: Only
15
- Classifier: Programming Language :: Python :: 3.8
16
- Classifier: Programming Language :: Python :: 3.9
17
14
  Classifier: Programming Language :: Python :: 3.10
18
15
  Classifier: Programming Language :: Python :: 3.11
19
16
  Classifier: Programming Language :: Python :: 3.12
20
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
21
19
  Classifier: Topic :: System :: Logging
22
20
  Classifier: Typing :: Typed
23
- Requires-Python: >=3.8
21
+ Requires-Python: >=3.10
24
22
  Description-Content-Type: text/markdown
25
23
  License-File: LICENSE
26
- License-File: NOTICE
27
- Requires-Dist: typing_extensions; python_version < "3.10"
28
- Provides-Extra: dev
29
- Requires-Dist: orjson; implementation_name != "pypy" and extra == "dev"
30
- Requires-Dist: msgspec; implementation_name != "pypy" and extra == "dev"
31
- Requires-Dist: validate-pyproject[all]; extra == "dev"
32
- Requires-Dist: black; extra == "dev"
33
- Requires-Dist: pylint; extra == "dev"
34
- Requires-Dist: mypy; extra == "dev"
35
- Requires-Dist: pytest; extra == "dev"
36
- Requires-Dist: freezegun; extra == "dev"
37
- Requires-Dist: backports.zoneinfo; python_version < "3.9" and extra == "dev"
38
- Requires-Dist: tzdata; extra == "dev"
39
- Requires-Dist: build; extra == "dev"
40
- Requires-Dist: mkdocs; extra == "dev"
41
- Requires-Dist: mkdocs-material>=8.5; extra == "dev"
42
- Requires-Dist: mkdocs-awesome-pages-plugin; extra == "dev"
43
- Requires-Dist: mdx_truly_sane_lists; extra == "dev"
44
- Requires-Dist: mkdocstrings[python]; extra == "dev"
45
- Requires-Dist: mkdocs-gen-files; extra == "dev"
46
- Requires-Dist: mkdocs-literate-nav; extra == "dev"
47
- Requires-Dist: mike; extra == "dev"
48
24
  Dynamic: license-file
49
25
 
50
26
  [![PyPi](https://img.shields.io/pypi/v/python-json-logger.svg)](https://pypi.python.org/pypi/python-json-logger/)
@@ -6,7 +6,6 @@ pyproject.toml
6
6
  src/python_json_logger.egg-info/PKG-INFO
7
7
  src/python_json_logger.egg-info/SOURCES.txt
8
8
  src/python_json_logger.egg-info/dependency_links.txt
9
- src/python_json_logger.egg-info/requires.txt
10
9
  src/python_json_logger.egg-info/top_level.txt
11
10
  src/pythonjsonlogger/__init__.py
12
11
  src/pythonjsonlogger/core.py
@@ -10,12 +10,8 @@ from datetime import datetime, timezone
10
10
  import logging
11
11
  import re
12
12
  import sys
13
- from typing import Optional, Union, List, Dict, Container, Any, Sequence
14
-
15
- if sys.version_info >= (3, 10):
16
- from typing import TypeAlias
17
- else:
18
- from typing_extensions import TypeAlias
13
+ from typing import TypeAlias, Any
14
+ from collections.abc import Container, Sequence
19
15
 
20
16
  ## Installed
21
17
 
@@ -24,7 +20,7 @@ else:
24
20
 
25
21
  ### CONSTANTS
26
22
  ### ============================================================================
27
- RESERVED_ATTRS: List[str] = [
23
+ RESERVED_ATTRS: list[str] = [
28
24
  "args",
29
25
  "asctime",
30
26
  "created",
@@ -65,13 +61,15 @@ if sys.version_info >= (3, 12):
65
61
  RESERVED_ATTRS.sort()
66
62
 
67
63
 
68
- STYLE_STRING_TEMPLATE_REGEX = re.compile(r"\$\{(.+?)\}", re.IGNORECASE) # $ style
64
+ STYLE_STRING_TEMPLATE_REGEX = re.compile(
65
+ r"\$(?:\$|\{(?P<braced>.+?)\}|(?P<named>[_a-z][_a-z0-9]*))", re.IGNORECASE
66
+ ) # $ style
69
67
  STYLE_STRING_FORMAT_REGEX = re.compile(r"\{(.+?)\}", re.IGNORECASE) # { style
70
68
  STYLE_PERCENT_REGEX = re.compile(r"%\((.+?)\)", re.IGNORECASE) # % style
71
69
 
72
70
  ## Type Aliases
73
71
  ## -----------------------------------------------------------------------------
74
- LogData: TypeAlias = Dict[str, Any]
72
+ LogData: TypeAlias = dict[str, Any]
75
73
  """Type alias
76
74
 
77
75
  *Changed in 4.0*: renamed from `LogRecord` to `LogData`
@@ -82,10 +80,10 @@ LogData: TypeAlias = Dict[str, Any]
82
80
  ### ============================================================================
83
81
  def merge_record_extra(
84
82
  record: logging.LogRecord,
85
- target: Dict,
83
+ target: dict[Any, Any],
86
84
  reserved: Container[str],
87
- rename_fields: Optional[Dict[str, str]] = None,
88
- ) -> Dict:
85
+ rename_fields: dict[str, str] | None = None,
86
+ ) -> dict[Any, Any]:
89
87
  """
90
88
  Merges extra attributes from LogRecord object into target dictionary
91
89
 
@@ -115,31 +113,27 @@ class BaseJsonFormatter(logging.Formatter):
115
113
  Must not be used directly.
116
114
 
117
115
  *New in 3.1*
118
-
119
- *Changed in 3.2*: `defaults` argument is no longer ignored.
120
-
121
- *Added in 3.3*: `exc_info_as_array` and `stack_info_as_array` options are added.
122
116
  """
123
117
 
124
- _style: Union[logging.PercentStyle, str] # type: ignore[assignment]
118
+ _style: logging.PercentStyle | str # type: ignore[assignment]
125
119
 
126
120
  ## Parent Methods
127
121
  ## -------------------------------------------------------------------------
128
122
  # pylint: disable=too-many-arguments,super-init-not-called
129
123
  def __init__(
130
124
  self,
131
- fmt: Optional[Union[str, Sequence[str]]] = None,
132
- datefmt: Optional[str] = None,
125
+ fmt: str | Sequence[str] | None = None,
126
+ datefmt: str | None = None,
133
127
  style: str = "%",
134
128
  validate: bool = True,
135
129
  *,
136
130
  prefix: str = "",
137
- rename_fields: Optional[Dict[str, str]] = None,
131
+ rename_fields: dict[str, str] | None = None,
138
132
  rename_fields_keep_missing: bool = False,
139
- static_fields: Optional[Dict[str, Any]] = None,
140
- reserved_attrs: Optional[Sequence[str]] = None,
141
- timestamp: Union[bool, str] = False,
142
- defaults: Optional[Dict[str, Any]] = None,
133
+ static_fields: dict[str, Any] | None = None,
134
+ reserved_attrs: Sequence[str] | None = None,
135
+ timestamp: bool | str = False,
136
+ defaults: dict[str, Any] | None = None,
143
137
  exc_info_as_array: bool = False,
144
138
  stack_info_as_array: bool = False,
145
139
  ) -> None:
@@ -176,16 +170,19 @@ class BaseJsonFormatter(logging.Formatter):
176
170
  missing fields. The original behaviour, missing fields have a value of `None`, is still
177
171
  available by setting `rename_fields_keep_missing` to `True`.
178
172
 
173
+ *Changed in 3.2*: `defaults` argument is no longer ignored.
174
+
175
+ *Added in 3.3*: `exc_info_as_array` and `stack_info_as_array` options are added.
176
+
179
177
  *Added in 4.0*:
180
178
 
181
- - `fmt` now supports comma seperated lists (`style=","`). Note that this style is specific
179
+ - `fmt` now supports comma separated lists (`style=","`). Note that this style is specific
182
180
  to `python-json-logger` and thus care should be taken to not to pass this format to other
183
181
  logging Formatter implementations.
184
182
  - `fmt` now supports sequences of strings (e.g. lists and tuples) of field names.
185
183
  """
186
184
  ## logging.Formatter compatibility
187
185
  ## ---------------------------------------------------------------------
188
- # Note: validate added in python 3.8, defaults added in 3.10
189
186
  if fmt is None or isinstance(fmt, str):
190
187
  if style in logging._STYLES:
191
188
  _style = logging._STYLES[style][0](fmt) # type: ignore[operator]
@@ -240,14 +237,20 @@ class BaseJsonFormatter(logging.Formatter):
240
237
  def format(self, record: logging.LogRecord) -> str:
241
238
  """Formats a log record and serializes to json
242
239
 
240
+ When `record.msg` is a `dict`, adding `exc_info` and `stack_info` does not
241
+ modify the caller's dict.
242
+
243
243
  Args:
244
244
  record: the record to format
245
+
246
+ *Changed in 4.2.0*: a `dict` `record.msg` is copied instead of modified
247
+ in place.
245
248
  """
246
- message_dict: Dict[str, Any] = {}
249
+ message_dict: dict[str, Any] = {}
247
250
  # TODO: logging.LogRecord.msg and logging.LogRecord.message in typeshed
248
251
  # are always type of str. We shouldn't need to override that.
249
252
  if isinstance(record.msg, dict):
250
- message_dict = record.msg
253
+ message_dict = record.msg.copy()
251
254
  record.message = ""
252
255
  else:
253
256
  record.message = record.getMessage()
@@ -276,7 +279,7 @@ class BaseJsonFormatter(logging.Formatter):
276
279
 
277
280
  ## JSON Formatter Specific Methods
278
281
  ## -------------------------------------------------------------------------
279
- def parse(self) -> List[str]:
282
+ def parse(self) -> list[str]:
280
283
  """Parses format string looking for substitutions
281
284
 
282
285
  This method is responsible for returning a list of fields (as strings)
@@ -300,20 +303,22 @@ class BaseJsonFormatter(logging.Formatter):
300
303
  raise ValueError(f"Style {self._style!r} is not supported")
301
304
 
302
305
  if isinstance(self._style, logging.StringTemplateStyle):
303
- formatter_style_pattern = STYLE_STRING_TEMPLATE_REGEX
306
+ # String templates support both ${name} and $name, and $$ is an escaped literal
307
+ return [
308
+ match.group("braced") or match.group("named")
309
+ for match in STYLE_STRING_TEMPLATE_REGEX.finditer(self._fmt)
310
+ if match.group("braced") or match.group("named")
311
+ ]
304
312
 
305
- elif isinstance(self._style, logging.StrFormatStyle):
306
- formatter_style_pattern = STYLE_STRING_FORMAT_REGEX
313
+ if isinstance(self._style, logging.StrFormatStyle):
314
+ return STYLE_STRING_FORMAT_REGEX.findall(self._fmt)
307
315
 
308
- elif isinstance(self._style, logging.PercentStyle):
316
+ if isinstance(self._style, logging.PercentStyle):
309
317
  # PercentStyle is parent class of StringTemplateStyle and StrFormatStyle
310
318
  # so it must be checked last.
311
- formatter_style_pattern = STYLE_PERCENT_REGEX
312
-
313
- else:
314
- raise ValueError(f"Style {self._style!r} is not supported")
319
+ return STYLE_PERCENT_REGEX.findall(self._fmt)
315
320
 
316
- return formatter_style_pattern.findall(self._fmt)
321
+ raise ValueError(f"Style {self._style!r} is not supported")
317
322
 
318
323
  def serialize_log_record(self, log_data: LogData) -> str:
319
324
  """Returns the final representation of the data to be logged
@@ -327,9 +332,9 @@ class BaseJsonFormatter(logging.Formatter):
327
332
 
328
333
  def add_fields(
329
334
  self,
330
- log_data: Dict[str, Any],
335
+ log_data: dict[str, Any],
331
336
  record: logging.LogRecord,
332
- message_dict: Dict[str, Any],
337
+ message_dict: dict[str, Any],
333
338
  ) -> None:
334
339
  """Extract fields from a LogRecord for logging
335
340
 
@@ -402,7 +407,7 @@ class BaseJsonFormatter(logging.Formatter):
402
407
  """
403
408
  return log_data
404
409
 
405
- def formatException(self, ei) -> Union[str, list[str]]: # type: ignore
410
+ def formatException(self, ei) -> str | list[str]: # type: ignore[override]
406
411
  """Format and return the specified exception information.
407
412
 
408
413
  If exc_info_as_array is set to True, This method returns an array of strings.
@@ -410,7 +415,7 @@ class BaseJsonFormatter(logging.Formatter):
410
415
  exception_info_str = super().formatException(ei)
411
416
  return exception_info_str.splitlines() if self.exc_info_as_array else exception_info_str
412
417
 
413
- def formatStack(self, stack_info) -> Union[str, list[str]]: # type: ignore
418
+ def formatStack(self, stack_info) -> str | list[str]: # type: ignore[override]
414
419
  """Format and return the specified stack information.
415
420
 
416
421
  If stack_info_as_array is set to True, This method returns an array of strings.
@@ -16,17 +16,11 @@ import base64
16
16
  import dataclasses
17
17
  import datetime
18
18
  import enum
19
- import sys
20
19
  from types import TracebackType
21
- from typing import Any
20
+ from typing import Any, TypeGuard
22
21
  import traceback
23
22
  import uuid
24
23
 
25
- if sys.version_info >= (3, 10):
26
- from typing import TypeGuard
27
- else:
28
- from typing_extensions import TypeGuard
29
-
30
24
  ## Installed
31
25
 
32
26
  ## Application
@@ -12,7 +12,8 @@ from __future__ import annotations
12
12
  ## Standard Library
13
13
  import datetime
14
14
  import json
15
- from typing import Any, Callable, Optional, Union
15
+ from typing import Any
16
+ from collections.abc import Callable
16
17
  import warnings
17
18
 
18
19
  ## Application
@@ -67,10 +68,10 @@ class JsonFormatter(core.BaseJsonFormatter):
67
68
  def __init__(
68
69
  self,
69
70
  *args,
70
- json_default: Optional[Callable] = None,
71
- json_encoder: Optional[Callable] = None,
71
+ json_default: Callable | None = None,
72
+ json_encoder: Callable | None = None,
72
73
  json_serializer: Callable = json.dumps,
73
- json_indent: Optional[Union[int, str]] = None,
74
+ json_indent: int | str | None = None,
74
75
  json_ensure_ascii: bool = True,
75
76
  **kwargs,
76
77
  ) -> None:
@@ -6,7 +6,8 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  ## Standard Library
9
- from typing import Any, Optional, Callable
9
+ from typing import Any
10
+ from collections.abc import Callable
10
11
 
11
12
  ## Installed
12
13
 
@@ -43,7 +44,7 @@ class MsgspecFormatter(core.BaseJsonFormatter):
43
44
  def __init__(
44
45
  self,
45
46
  *args,
46
- json_default: Optional[Callable] = msgspec_default,
47
+ json_default: Callable | None = msgspec_default,
47
48
  **kwargs,
48
49
  ) -> None:
49
50
  """
@@ -6,7 +6,8 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  ## Standard Library
9
- from typing import Any, Optional, Callable
9
+ from typing import Any
10
+ from collections.abc import Callable
10
11
 
11
12
  ## Installed
12
13
 
@@ -15,7 +16,7 @@ from . import core
15
16
  from . import defaults as d
16
17
  from .utils import package_is_available
17
18
 
18
- # We import msgspec after checking it is available
19
+ # We import orjson after checking it is available
19
20
  package_is_available("orjson", throw_error=True)
20
21
  import orjson # pylint: disable=wrong-import-position,wrong-import-order
21
22
 
@@ -45,7 +46,7 @@ class OrjsonFormatter(core.BaseJsonFormatter):
45
46
  def __init__(
46
47
  self,
47
48
  *args,
48
- json_default: Optional[Callable] = orjson_default,
49
+ json_default: Callable | None = orjson_default,
49
50
  json_indent: bool = False,
50
51
  **kwargs,
51
52
  ) -> None:
@@ -9,7 +9,8 @@ import io
9
9
  import json
10
10
  import logging
11
11
  import logging.config
12
- from typing import Any, Generator
12
+ from typing import Any
13
+ from collections.abc import Generator
13
14
 
14
15
  ## Installed
15
16
  import pytest
@@ -64,7 +65,7 @@ class LoggingEnvironment:
64
65
 
65
66
 
66
67
  @pytest.fixture
67
- def env() -> Generator[LoggingEnvironment, None, None]:
68
+ def env() -> Generator[LoggingEnvironment]:
68
69
  global _LOGGER_COUNT # pylint: disable=global-statement
69
70
  _LOGGER_COUNT += 1
70
71
  logging.config.dictConfig(LOGGING_CONFIG)
@@ -13,13 +13,10 @@ import logging
13
13
  import sys
14
14
  import traceback
15
15
  from types import TracebackType
16
- from typing import Any, Generator
16
+ from typing import Any
17
+ from collections.abc import Generator
17
18
  import uuid
18
-
19
- if sys.version_info >= (3, 9):
20
- import zoneinfo
21
- else:
22
- from backports import zoneinfo
19
+ import zoneinfo
23
20
 
24
21
  ## Installed
25
22
  import freezegun
@@ -63,7 +60,7 @@ class LoggingEnvironment:
63
60
 
64
61
 
65
62
  @pytest.fixture
66
- def env() -> Generator[LoggingEnvironment, None, None]:
63
+ def env() -> Generator[LoggingEnvironment]:
67
64
  global _LOGGER_COUNT # pylint: disable=global-statement
68
65
  _LOGGER_COUNT += 1
69
66
  logger = logging.getLogger(f"pythonjsonlogger.tests.{_LOGGER_COUNT}")
@@ -170,6 +167,22 @@ def test_percentage_format(env: LoggingEnvironment, class_: type[BaseJsonFormatt
170
167
  return
171
168
 
172
169
 
170
+ @pytest.mark.parametrize("class_", ALL_FORMATTERS)
171
+ def test_string_template_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
172
+ # Note: string templates support both $name and ${name}, and $$ is an escaped literal $
173
+ env.set_formatter(
174
+ class_("$$literal $levelname ${message} $filename ${lineno} $asctime", style="$")
175
+ )
176
+
177
+ msg = "testing logging format"
178
+ env.logger.info(msg)
179
+ log_json = env.load_json()
180
+
181
+ assert log_json["message"] == msg
182
+ assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"}
183
+ return
184
+
185
+
173
186
  @pytest.mark.parametrize("class_", ALL_FORMATTERS)
174
187
  def test_comma_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
175
188
  # Note: we have double comma `,,` to test handling "empty" names
@@ -396,6 +409,23 @@ def test_log_dict_defaults(env: LoggingEnvironment, class_: type[BaseJsonFormatt
396
409
  return
397
410
 
398
411
 
412
+ @pytest.mark.parametrize("class_", ALL_FORMATTERS)
413
+ def test_log_dict_not_modified(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
414
+ env.set_formatter(class_())
415
+
416
+ msg = {"text": "testing logging", "nested": {"more": "data"}}
417
+ try:
418
+ raise ValueError("test")
419
+ except ValueError:
420
+ env.logger.exception(msg, stack_info=True)
421
+ log_json = env.load_json()
422
+
423
+ assert log_json["exc_info"]
424
+ assert log_json["stack_info"]
425
+ assert msg == {"text": "testing logging", "nested": {"more": "data"}}
426
+ return
427
+
428
+
399
429
  @pytest.mark.parametrize("class_", ALL_FORMATTERS)
400
430
  def test_log_extra(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
401
431
  env.set_formatter(class_())
@@ -558,6 +588,7 @@ def test_default_encoder_with_timestamp(env: LoggingEnvironment, class_: type[Ba
558
588
  (False, bool, False),
559
589
  (None, type(None), None),
560
590
  (b"some-bytes", str, "c29tZS1ieXRlcw=="),
591
+ (b"fancy-bytes-\xf0\xf1", str, "ZmFuY3ktYnl0ZXMt8PE="),
561
592
  (datetime.time(16, 45, 30, 100), str, "16:45:30.000100"),
562
593
  (datetime.date(2024, 5, 5), str, "2024-05-05"),
563
594
  (datetime.datetime(2024, 5, 5, 16, 45, 30, 100), str, "2024-05-05T16:45:30.000100"),
@@ -683,6 +714,148 @@ def test_exc_info_as_array_no_exc_info(env: LoggingEnvironment, class_: type[Bas
683
714
  return
684
715
 
685
716
 
717
+ @pytest.mark.skipif(
718
+ sys.version_info < (3, 11), reason="Exception notes require Python 3.11 or higher"
719
+ )
720
+ @pytest.mark.parametrize("class_", ALL_FORMATTERS)
721
+ def test_exception_notes(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
722
+ env.set_formatter(class_())
723
+
724
+ try:
725
+ err = ValueError("my error")
726
+ err.add_note("this is a note")
727
+ raise err
728
+ except ValueError:
729
+ env.logger.exception("oops")
730
+
731
+ log_json = env.load_json()
732
+ assert "ValueError: my error" in log_json["exc_info"]
733
+ assert "this is a note" in log_json["exc_info"]
734
+ return
735
+
736
+
737
+ @pytest.mark.skipif(
738
+ sys.version_info < (3, 11), reason="Exception notes require Python 3.11 or higher"
739
+ )
740
+ @pytest.mark.parametrize("class_", ALL_FORMATTERS)
741
+ def test_exception_notes_as_array(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
742
+ env.set_formatter(class_(exc_info_as_array=True))
743
+
744
+ try:
745
+ err = ValueError("my error")
746
+ err.add_note("this is a note")
747
+ raise err
748
+ except ValueError:
749
+ env.logger.exception("oops")
750
+
751
+ log_json = env.load_json()
752
+ assert isinstance(log_json["exc_info"], list)
753
+ full_traceback = "\n".join(log_json["exc_info"])
754
+ assert "ValueError: my error" in full_traceback
755
+ assert "this is a note" in full_traceback
756
+ return
757
+
758
+
759
+ @pytest.mark.skipif(
760
+ sys.version_info < (3, 11), reason="Exception groups require Python 3.11 or higher"
761
+ )
762
+ @pytest.mark.parametrize("class_", ALL_FORMATTERS)
763
+ def test_exception_groups(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
764
+ env.set_formatter(class_())
765
+
766
+ try:
767
+ raise ExceptionGroup("my group", [ValueError("err 1"), TypeError("err 2")])
768
+ except ExceptionGroup:
769
+ env.logger.exception("oops")
770
+
771
+ log_json = env.load_json()
772
+ assert "ExceptionGroup: my group" in log_json["exc_info"]
773
+ assert "ValueError: err 1" in log_json["exc_info"]
774
+ assert "TypeError: err 2" in log_json["exc_info"]
775
+ return
776
+
777
+
778
+ @pytest.mark.skipif(
779
+ sys.version_info < (3, 11), reason="Exception groups require Python 3.11 or higher"
780
+ )
781
+ @pytest.mark.parametrize("class_", ALL_FORMATTERS)
782
+ def test_exception_groups_as_array(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
783
+ env.set_formatter(class_(exc_info_as_array=True))
784
+
785
+ try:
786
+ raise ExceptionGroup("my group", [ValueError("err 1"), TypeError("err 2")])
787
+ except ExceptionGroup:
788
+ env.logger.exception("oops")
789
+
790
+ log_json = env.load_json()
791
+ assert isinstance(log_json["exc_info"], list)
792
+ full_traceback = "\n".join(log_json["exc_info"])
793
+ assert "ExceptionGroup: my group" in full_traceback
794
+ assert "ValueError: err 1" in full_traceback
795
+ assert "TypeError: err 2" in full_traceback
796
+ return
797
+
798
+
799
+ @pytest.mark.skipif(
800
+ sys.version_info < (3, 11), reason="Exception groups and notes require Python 3.11 or higher"
801
+ )
802
+ @pytest.mark.parametrize("class_", ALL_FORMATTERS)
803
+ def test_exception_groups_with_notes(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
804
+ env.set_formatter(class_())
805
+
806
+ try:
807
+ err1 = ValueError("err 1")
808
+ err1.add_note("note 1")
809
+ err2 = TypeError("err 2")
810
+ err2.add_note("note 2")
811
+ eg = ExceptionGroup("my group", [err1, err2])
812
+ eg.add_note("group note")
813
+ raise eg
814
+ except ExceptionGroup:
815
+ env.logger.exception("oops")
816
+
817
+ log_json = env.load_json()
818
+ assert "ExceptionGroup: my group" in log_json["exc_info"]
819
+ assert "group note" in log_json["exc_info"]
820
+ assert "ValueError: err 1" in log_json["exc_info"]
821
+ assert "note 1" in log_json["exc_info"]
822
+ assert "TypeError: err 2" in log_json["exc_info"]
823
+ assert "note 2" in log_json["exc_info"]
824
+ return
825
+
826
+
827
+ @pytest.mark.skipif(
828
+ sys.version_info < (3, 11), reason="Exception groups and notes require Python 3.11 or higher"
829
+ )
830
+ @pytest.mark.parametrize("class_", ALL_FORMATTERS)
831
+ def test_exception_groups_with_notes_as_array(
832
+ env: LoggingEnvironment, class_: type[BaseJsonFormatter]
833
+ ):
834
+ env.set_formatter(class_(exc_info_as_array=True))
835
+
836
+ try:
837
+ err1 = ValueError("err 1")
838
+ err1.add_note("note 1")
839
+ err2 = TypeError("err 2")
840
+ err2.add_note("note 2")
841
+ eg = ExceptionGroup("my group", [err1, err2])
842
+ eg.add_note("group note")
843
+ raise eg
844
+ except ExceptionGroup:
845
+ env.logger.exception("oops")
846
+
847
+ log_json = env.load_json()
848
+ assert isinstance(log_json["exc_info"], list)
849
+ full_traceback = "\n".join(log_json["exc_info"])
850
+ assert "ExceptionGroup: my group" in full_traceback
851
+ assert "group note" in full_traceback
852
+ assert "ValueError: err 1" in full_traceback
853
+ assert "note 1" in full_traceback
854
+ assert "TypeError: err 2" in full_traceback
855
+ assert "note 2" in full_traceback
856
+ return
857
+
858
+
686
859
  @pytest.mark.parametrize("class_", ALL_FORMATTERS)
687
860
  def test_stack_info_as_array(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
688
861
  env.set_formatter(class_(stack_info_as_array=True))
@@ -1,28 +0,0 @@
1
-
2
- [:python_version < "3.10"]
3
- typing_extensions
4
-
5
- [dev]
6
- validate-pyproject[all]
7
- black
8
- pylint
9
- mypy
10
- pytest
11
- freezegun
12
- tzdata
13
- build
14
- mkdocs
15
- mkdocs-material>=8.5
16
- mkdocs-awesome-pages-plugin
17
- mdx_truly_sane_lists
18
- mkdocstrings[python]
19
- mkdocs-gen-files
20
- mkdocs-literate-nav
21
- mike
22
-
23
- [dev:implementation_name != "pypy"]
24
- orjson
25
- msgspec
26
-
27
- [dev:python_version < "3.9"]
28
- backports.zoneinfo