Skip to content
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
10 changes: 7 additions & 3 deletions src/structlog/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
42 changes: 38 additions & 4 deletions tests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
23 changes: 23 additions & 0 deletions tests/test_stdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
"""
Expand Down