diff --git a/src/structlog/_output.py b/src/structlog/_output.py index b1612de6..da5495cf 100644 --- a/src/structlog/_output.py +++ b/src/structlog/_output.py @@ -256,13 +256,17 @@ class BytesLogger: .. versionadded:: 20.2.0 """ - __slots__ = ("_file", "_flush", "_lock", "_write") + __slots__ = ("_file", "_flush", "_lock", "_write", "name") - def __init__(self, file: BinaryIO | None = None): + def __init__( + self, file: BinaryIO | None = None, *, name: str | None = None + ): self._file = file or sys.stdout.buffer self._write = self._file.write self._flush = self._file.flush + self.name = name + self._lock = _get_lock_for_file(self._file) def __getstate__(self) -> str: @@ -345,4 +349,4 @@ def __init__(self, file: BinaryIO | None = None): self._file = file def __call__(self, *args: Any) -> BytesLogger: - return BytesLogger(self._file) + return BytesLogger(self._file, name=args[0] if args else None) diff --git a/tests/test_output.py b/tests/test_output.py index 8553aad9..c298dd9d 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -295,6 +295,22 @@ def test_deepcopy(self, capsys): assert "hello\n" == out assert "" == err + def test_name_attribute(self): + """ + BytesLogger accepts a name keyword argument. + """ + bl = BytesLogger(name="test_logger") + + assert "test_logger" == bl.name + + def test_name_defaults_to_none(self): + """ + BytesLogger name defaults to None when not provided. + """ + bl = BytesLogger() + + assert bl.name is None + def test_deepcopy_no_stdout(self, tmp_path): """ Only BytesLoggers that log to stdout or stderr can be deepcopy-ed. @@ -327,9 +343,27 @@ def test_passes_file(self): assert stderr is pl._file - def test_ignores_args(self): + def test_passes_name_from_args(self): """ - BytesLogger doesn't take positional arguments. If any are passed to - the factory, they are not passed to the logger. + The first positional argument is used as the logger name. """ - BytesLoggerFactory()(1, 2, 3) + bl = BytesLoggerFactory()("my_logger") + + assert "my_logger" == bl.name + + def test_name_defaults_to_none(self): + """ + If no positional arguments are passed to the factory, the logger + name defaults to None. + """ + bl = BytesLoggerFactory()() + + assert bl.name is None + + def test_extra_args_ignored(self): + """ + Positional arguments beyond the first are silently ignored. + """ + bl = BytesLoggerFactory()("my_logger", 2, 3) + + assert "my_logger" == bl.name diff --git a/tests/test_stdlib.py b/tests/test_stdlib.py index f54ae6b2..0488b4da 100644 --- a/tests/test_stdlib.py +++ b/tests/test_stdlib.py @@ -569,6 +569,29 @@ def test_logger_name_added_with_record(self, make_log_record): assert name == event_dict["logger"] + def test_logger_name_added_with_bytes_logger(self): + """ + add_logger_name works with BytesLogger that has a name attribute. + """ + from structlog import BytesLogger + + name = "sample-name" + logger = BytesLogger(name=name) + event_dict = add_logger_name(logger, None, {}) + + assert name == event_dict["logger"] + + def test_logger_name_none_with_unnamed_bytes_logger(self): + """ + add_logger_name works with BytesLogger without a name, returning None. + """ + from structlog import BytesLogger + + logger = BytesLogger() + event_dict = add_logger_name(logger, None, {}) + + assert event_dict["logger"] is None + def extra_dict() -> dict[str, Any]: """