Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/

### Added

- Added `structlog.dev.ConsoleRenderer.get_default_column_styles` for reuse the default column styles.
[#741](https://github.com/hynek/structlog/pull/741)

- `structlog.testing.capture_logs()` now optionally accepts *processors* to apply before capture.
[#728](https://github.com/hynek/structlog/pull/728)

Expand Down
2 changes: 1 addition & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ API Reference
.. automodule:: structlog.dev

.. autoclass:: ConsoleRenderer
:members: get_default_level_styles
:members: get_default_level_styles, get_default_column_styles

.. autoclass:: Column
.. autoclass:: ColumnFormatter(typing.Protocol)
Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
("py:class", "PlainFileObserver"),
("py:class", "Processor"),
("py:class", "Styles"),
("py:class", "structlog.dev._Styles"),
("py:class", "WrappedLogger"),
("py:class", "structlog.threadlocal.TLLogger"),
("py:class", "structlog.typing.EventDict"),
Expand Down
73 changes: 49 additions & 24 deletions src/structlog/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ class ConsoleRenderer:
.. versionadded:: 24.2.0 *pad_level*
"""

def __init__( # noqa: PLR0912, PLR0915
def __init__(
self,
pad_event: int = _EVENT_WIDTH,
colors: bool = _has_colors,
Expand Down Expand Up @@ -630,29 +630,7 @@ def add_meaningless_arg(arg: str) -> None:
return

# Create default columns configuration.
styles: Styles
if colors:
if _IS_WINDOWS: # pragma: no cover
# On Windows, we can't do colorful output without colorama.
if colorama is None:
classname = self.__class__.__name__
raise SystemError(
_MISSING.format(
who=classname + " with `colors=True`",
package="colorama",
)
)
# Colorama must be init'd on Windows, but must NOT be
# init'd on other OSes, because it can break colors.
if force_colors:
colorama.deinit()
colorama.init(strip=False)
else:
colorama.init()

styles = _ColorfulStyles
else:
styles = _PlainStyles
styles = self.get_default_column_styles(colors, force_colors)

self._styles = styles

Expand Down Expand Up @@ -719,6 +697,53 @@ def add_meaningless_arg(arg: str) -> None:
Column("logger_name", logger_name_formatter),
]

@classmethod
def get_default_column_styles(
cls, colors: bool = _has_colors, force_colors: bool = False
) -> Styles:
"""
Configure and return the appropriate styles class for console output.

This method handles the setup of colorful or plain styles, including
proper colorama initialization on Windows systems when colors are enabled.

Args:
colors: Whether to use colorful output styles.

force_colors:
Force colorful output even in non-interactive environments.
Only relevant on Windows with colorama.

Returns:
The configured styles class (_ColorfulStyles or _PlainStyles).

Raises:
SystemError: On Windows when colors=True but colorama is not installed.

.. versionadded:: 25.5.0
"""
if not colors:
return _PlainStyles

if _IS_WINDOWS: # pragma: no cover
# On Windows, we can't do colorful output without colorama.
if colorama is None:
raise SystemError(
_MISSING.format(
who=cls.__name__ + " with `colors=True`",
package="colorama",
)
)
# Colorama must be init'd on Windows, but must NOT be
# init'd on other OSes, because it can break colors.
if force_colors:
colorama.deinit()
colorama.init(strip=False)
else:
colorama.init()

return _ColorfulStyles

def _repr(self, val: Any) -> str:
"""
Determine representation of *val* depending on its type &
Expand Down
58 changes: 58 additions & 0 deletions tests/test_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,64 @@ def test_init_accepts_overriding_levels(self, styles, padded):
+ styles.reset
) == rv

def test_returns_colorful_styles_when_colors_true(self):
"""
When colors=True, returns _ColorfulStyles class.
"""
styles = dev.ConsoleRenderer.get_default_column_styles(colors=True)

assert styles is dev._ColorfulStyles
assert hasattr(styles, "reset")
assert hasattr(styles, "bright")
assert hasattr(styles, "level_critical")
assert hasattr(styles, "kv_key")
assert hasattr(styles, "kv_value")

def test_returns_plain_styles_when_colors_false(self):
"""
When colors=False, returns _PlainStyles class.
"""
styles = dev.ConsoleRenderer.get_default_column_styles(colors=False)

assert styles is dev._PlainStyles
assert styles.reset == ""
assert styles.bright == ""
assert styles.level_critical == ""
assert styles.kv_key == ""
assert styles.kv_value == ""

@pytest.mark.skipif(
not dev._IS_WINDOWS or dev.colorama is not None,
reason="Only relevant on Windows without colorama",
)
def test_raises_system_error_on_windows_without_colorama(self):
"""
On Windows without colorama, raises SystemError when colors=True.
"""
with pytest.raises(SystemError, match="requires the colorama package"):
dev.ConsoleRenderer.get_default_column_styles(colors=True)

@pytest.mark.skipif(
not dev._IS_WINDOWS or dev.colorama is None,
reason="Only relevant on Windows with colorama",
)
def test_initializes_colorama_on_windows_with_force_colors(self):
"""
On Windows with colorama, force_colors=True reinitializes colorama.
"""
with mock.patch.object(
dev.colorama, "init"
) as mock_init, mock.patch.object(
dev.colorama, "deinit"
) as mock_deinit:
styles = dev.ConsoleRenderer.get_default_column_styles(
colors=True, force_colors=True
)

assert styles is dev._ColorfulStyles
mock_deinit.assert_called_once()
mock_init.assert_called_once_with(strip=False)

def test_logger_name(self, cr, styles, padded):
"""
Logger names are appended after the event.
Expand Down
Loading