Skip to content

[core] Support sequence of strings for Formatter fmt #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: nhairs-comma-format
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Allows using values like `ext://sys.stderr` in `fileConfig`/`dictConfig` value fields.
- Support comma seperated lists for Formatter `fmt` (`style=","`) e.g. `"asctime,message,levelname"` [#15](https://github.com/nhairs/python-json-logger/issues/15)
- 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.
- Supports sequences of strings (e.g. lists and tuples) of field names for Formatter `fmt`.

### Changed
- 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)
Expand Down
60 changes: 38 additions & 22 deletions src/pythonjsonlogger/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class BaseJsonFormatter(logging.Formatter):
# pylint: disable=too-many-arguments,super-init-not-called
def __init__(
self,
fmt: Optional[str] = None,
fmt: Optional[Union[str, Sequence[str]]] = None,
datefmt: Optional[str] = None,
style: str = "%",
validate: bool = True,
Expand All @@ -145,11 +145,11 @@ def __init__(
) -> None:
"""
Args:
fmt: string representing fields to log
fmt: String format or `Sequence` of field names of fields to log.
datefmt: format to use when formatting `asctime` field
style: how to extract log fields from `fmt`
style: how to extract log fields from `fmt`. Ignored if `fmt` is a `Sequence[str]`.
validate: validate `fmt` against style, if implementing a custom `style` you
must set this to `False`.
must set this to `False`. Ignored if `fmt` is a `Sequence[str]`.
defaults: a dictionary containing default fields that are added before all other fields and
may be overridden. The supplied fields are still subject to `rename_fields`.
prefix: an optional string prefix added at the beginning of
Expand Down Expand Up @@ -181,23 +181,34 @@ def __init__(
- `fmt` now supports comma seperated lists (`style=","`). Note that this style is specific
to `python-json-logger` and thus care should be taken to not to pass this format to other
logging Formatter implementations.
- `fmt` now supports sequences of strings (e.g. lists and tuples) of field names.
"""
## logging.Formatter compatibility
## ---------------------------------------------------------------------
# Note: validate added in 3.8, defaults added in 3.10
if style in logging._STYLES:
_style = logging._STYLES[style][0](fmt) # type: ignore[operator]
if validate:
_style.validate()
self._style = _style
self._fmt = _style._fmt

elif style == "," or not validate:
self._style = style
self._fmt = fmt
if fmt is None or isinstance(fmt, str):
if style in logging._STYLES:
_style = logging._STYLES[style][0](fmt) # type: ignore[operator]
if validate:
_style.validate()
self._style = _style
self._fmt = _style._fmt

else:
raise ValueError(f"Style must be one of: {','.join(logging._STYLES.keys())}")
elif style == "," or not validate:
self._style = style
self._fmt = fmt

else:
raise ValueError(f"Style must be one of: {','.join(logging._STYLES.keys())}")

self._required_fields = self.parse()

# Note: we do this check second as string is still a Sequence[str]
elif isinstance(fmt, Sequence):
self._style = "__sequence__"
self._fmt = str(fmt)
self._required_fields = list(fmt)

self.datefmt = datefmt

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

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

if isinstance(self._style, str) and self._style == ",":
# TODO: should we check that there are no empty fields?
# If yes we should do this in __init__ where we validate other styles?
# Do we drop empty fields?
# etc
return [field.strip() for field in self._fmt.split(",") if field.strip()]
if isinstance(self._style, str):
if self._style == "__sequence__":
raise RuntimeError("Must not call parse when fmt is a sequence of strings")

if self._style == ",":
# TODO: should we check that there are no empty fields?
# If yes we should do this in __init__ where we validate other styles?
# Do we drop empty fields?
# etc
return [field.strip() for field in self._fmt.split(",") if field.strip()]

raise ValueError(f"Style {self._style!r} is not supported")

if isinstance(self._style, logging.StringTemplateStyle):
formatter_style_pattern = STYLE_STRING_TEMPLATE_REGEX
Expand Down
26 changes: 26 additions & 0 deletions tests/test_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,32 @@ def test_comma_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
return


@pytest.mark.parametrize("class_", ALL_FORMATTERS)
def test_sequence_list_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
env.set_formatter(class_(["levelname", "message", "filename", "lineno", "asctime"]))

msg = "testing logging format"
env.logger.info(msg)
log_json = env.load_json()

assert log_json["message"] == msg
assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"}
return


@pytest.mark.parametrize("class_", ALL_FORMATTERS)
def test_sequence_tuple_format(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
env.set_formatter(class_(("levelname", "message", "filename", "lineno", "asctime")))

msg = "testing logging format"
env.logger.info(msg)
log_json = env.load_json()

assert log_json["message"] == msg
assert log_json.keys() == {"levelname", "message", "filename", "lineno", "asctime"}
return


@pytest.mark.parametrize("class_", ALL_FORMATTERS)
def test_defaults_field(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
env.set_formatter(class_(defaults={"first": 1, "second": 2}))
Expand Down