Skip to content

Commit 97decbd

Browse files
committed
[core] Support sequence of strings for Formatter fmt
Fixes: #16
1 parent 8ffd19d commit 97decbd

File tree

3 files changed

+65
-22
lines changed

3 files changed

+65
-22
lines changed

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- Allows using values like `ext://sys.stderr` in `fileConfig`/`dictConfig` value fields.
1212
- Support comma seperated lists for Formatter `fmt` (`style=","`) e.g. `"asctime,message,levelname"` [#15](https://github.com/nhairs/python-json-logger/issues/15)
1313
- Note that this style is specific to `python-json-logger` and thus care should be taken not to pass this format to other logging Formatter implementations.
14+
- Supports sequences of strings (e.g. lists and tuples) of field names for Formatter `fmt`.
1415

1516
### Changed
1617
- Rename `pythonjsonlogger.core.LogRecord` and `log_record` arguemtns to avoid confusion / overlapping with `logging.LogRecord`. [#38](https://github.com/nhairs/python-json-logger/issues/38)

src/pythonjsonlogger/core.py

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ class BaseJsonFormatter(logging.Formatter):
128128
# pylint: disable=too-many-arguments,super-init-not-called
129129
def __init__(
130130
self,
131-
fmt: Optional[str] = None,
131+
fmt: Optional[Union[str, Sequence[str]]] = None,
132132
datefmt: Optional[str] = None,
133133
style: str = "%",
134134
validate: bool = True,
@@ -145,11 +145,11 @@ def __init__(
145145
) -> None:
146146
"""
147147
Args:
148-
fmt: string representing fields to log
148+
fmt: String format or `Sequence` of field names of fields to log.
149149
datefmt: format to use when formatting `asctime` field
150-
style: how to extract log fields from `fmt`
150+
style: how to extract log fields from `fmt`. Ignored if `fmt` is a `Sequence[str]`.
151151
validate: validate `fmt` against style, if implementing a custom `style` you
152-
must set this to `False`.
152+
must set this to `False`. Ignored if `fmt` is a `Sequence[str]`.
153153
defaults: a dictionary containing default fields that are added before all other fields and
154154
may be overridden. The supplied fields are still subject to `rename_fields`.
155155
prefix: an optional string prefix added at the beginning of
@@ -181,23 +181,34 @@ def __init__(
181181
- `fmt` now supports comma seperated lists (`style=","`). Note that this style is specific
182182
to `python-json-logger` and thus care should be taken to not to pass this format to other
183183
logging Formatter implementations.
184+
- `fmt` now supports sequences of strings (e.g. lists and tuples) of field names.
184185
"""
185186
## logging.Formatter compatibility
186187
## ---------------------------------------------------------------------
187188
# Note: validate added in 3.8, defaults added in 3.10
188-
if style in logging._STYLES:
189-
_style = logging._STYLES[style][0](fmt) # type: ignore[operator]
190-
if validate:
191-
_style.validate()
192-
self._style = _style
193-
self._fmt = _style._fmt
194189

195-
elif style == "," or not validate:
196-
self._style = style
197-
self._fmt = fmt
190+
if fmt is None or isinstance(fmt, str):
191+
if style in logging._STYLES:
192+
_style = logging._STYLES[style][0](fmt) # type: ignore[operator]
193+
if validate:
194+
_style.validate()
195+
self._style = _style
196+
self._fmt = _style._fmt
198197

199-
else:
200-
raise ValueError(f"Style must be one of: {','.join(logging._STYLES.keys())}")
198+
elif style == "," or not validate:
199+
self._style = style
200+
self._fmt = fmt
201+
202+
else:
203+
raise ValueError(f"Style must be one of: {','.join(logging._STYLES.keys())}")
204+
205+
self._required_fields = self.parse()
206+
207+
# Note: we do this check second as string is still a Sequence[str]
208+
elif isinstance(fmt, Sequence):
209+
self._style = "__sequence__"
210+
self._fmt = str(fmt)
211+
self._required_fields = list(fmt)
201212

202213
self.datefmt = datefmt
203214

@@ -219,7 +230,6 @@ def __init__(
219230
self.reserved_attrs = set(reserved_attrs if reserved_attrs is not None else RESERVED_ATTRS)
220231
self.timestamp = timestamp
221232

222-
self._required_fields = self.parse()
223233
self._skip_fields = set(self._required_fields)
224234
self._skip_fields.update(self.reserved_attrs)
225235
self.defaults = defaults if defaults is not None else {}
@@ -282,12 +292,18 @@ def parse(self) -> List[str]:
282292
# (we already (mostly) check for valid style names in __init__
283293
return []
284294

285-
if isinstance(self._style, str) and self._style == ",":
286-
# TODO: should we check that there are no empty fields?
287-
# If yes we should do this in __init__ where we validate other styles?
288-
# Do we drop empty fields?
289-
# etc
290-
return [field.strip() for field in self._fmt.split(",") if field.strip()]
295+
if isinstance(self._style, str):
296+
if self._style == "__sequence__":
297+
raise RuntimeError("Must not call parse when fmt is a sequence of strings")
298+
299+
if self._style == ",":
300+
# TODO: should we check that there are no empty fields?
301+
# If yes we should do this in __init__ where we validate other styles?
302+
# Do we drop empty fields?
303+
# etc
304+
return [field.strip() for field in self._fmt.split(",") if field.strip()]
305+
306+
raise ValueError(f"Style {self._style!r} is not supported")
291307

292308
if isinstance(self._style, logging.StringTemplateStyle):
293309
formatter_style_pattern = STYLE_STRING_TEMPLATE_REGEX

tests/test_formatters.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,32 @@ def test_comma_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
184184
return
185185

186186

187+
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
188+
def test_sequence_list_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
189+
env.set_formatter(class_(["levelname", "message", "filename", "lineno", "asctime"]))
190+
191+
msg = "testing logging format"
192+
env.logger.info(msg)
193+
log_json = env.load_json()
194+
195+
assert log_json["message"] == msg
196+
assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"}
197+
return
198+
199+
200+
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
201+
def test_sequence_tuple_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
202+
env.set_formatter(class_(("levelname", "message", "filename", "lineno", "asctime")))
203+
204+
msg = "testing logging format"
205+
env.logger.info(msg)
206+
log_json = env.load_json()
207+
208+
assert log_json["message"] == msg
209+
assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"}
210+
return
211+
212+
187213
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
188214
def test_defaults_field(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
189215
env.set_formatter(class_(defaults={"first": 1, "second": 2}))

0 commit comments

Comments
 (0)