Skip to content

Commit 9d4022c

Browse files
refactor: extract styles configuration to separate method (hynek#741)
* refactor: extract styles configuration to separate method Extract the styles configuration logic from ConsoleRenderer.__init__ into a dedicated configure_styles class method. This improves code readability and makes the styles setup logic reusable and testable. - Move colorama initialization and styles selection to configure_styles() - Add comprehensive docstring with args, returns, and raises sections - Remove noqa comment as method complexity is now reduced - Maintain existing behavior for colors and force_colors parameters * docs, tests * docs: versionadded * docs: changelog.md * fix: _Styles -> Styles * Fix type `Styles` already includes `Type[_Styles]`, so `type[Styles]` should be wrong * Fix Sphinx build --------- Co-authored-by: Hynek Schlawack <hs@ox.cx>
1 parent 83d7cdc commit 9d4022c

File tree

5 files changed

+112
-25
lines changed

5 files changed

+112
-25
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
1717

1818
### Added
1919

20+
- Added `structlog.dev.ConsoleRenderer.get_default_column_styles` for reuse the default column styles.
21+
[#741](https://github.com/hynek/structlog/pull/741)
22+
2023
- `structlog.testing.capture_logs()` now optionally accepts *processors* to apply before capture.
2124
[#728](https://github.com/hynek/structlog/pull/728)
2225

docs/api.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ API Reference
7676
.. automodule:: structlog.dev
7777

7878
.. autoclass:: ConsoleRenderer
79-
:members: get_default_level_styles
79+
:members: get_default_level_styles, get_default_column_styles
8080

8181
.. autoclass:: Column
8282
.. autoclass:: ColumnFormatter(typing.Protocol)

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
("py:class", "PlainFileObserver"),
8484
("py:class", "Processor"),
8585
("py:class", "Styles"),
86+
("py:class", "structlog.dev._Styles"),
8687
("py:class", "WrappedLogger"),
8788
("py:class", "structlog.threadlocal.TLLogger"),
8889
("py:class", "structlog.typing.EventDict"),

src/structlog/dev.py

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ class ConsoleRenderer:
567567
.. versionadded:: 24.2.0 *pad_level*
568568
"""
569569

570-
def __init__( # noqa: PLR0912, PLR0915
570+
def __init__(
571571
self,
572572
pad_event: int = _EVENT_WIDTH,
573573
colors: bool = _has_colors,
@@ -630,29 +630,7 @@ def add_meaningless_arg(arg: str) -> None:
630630
return
631631

632632
# Create default columns configuration.
633-
styles: Styles
634-
if colors:
635-
if _IS_WINDOWS: # pragma: no cover
636-
# On Windows, we can't do colorful output without colorama.
637-
if colorama is None:
638-
classname = self.__class__.__name__
639-
raise SystemError(
640-
_MISSING.format(
641-
who=classname + " with `colors=True`",
642-
package="colorama",
643-
)
644-
)
645-
# Colorama must be init'd on Windows, but must NOT be
646-
# init'd on other OSes, because it can break colors.
647-
if force_colors:
648-
colorama.deinit()
649-
colorama.init(strip=False)
650-
else:
651-
colorama.init()
652-
653-
styles = _ColorfulStyles
654-
else:
655-
styles = _PlainStyles
633+
styles = self.get_default_column_styles(colors, force_colors)
656634

657635
self._styles = styles
658636

@@ -719,6 +697,53 @@ def add_meaningless_arg(arg: str) -> None:
719697
Column("logger_name", logger_name_formatter),
720698
]
721699

700+
@classmethod
701+
def get_default_column_styles(
702+
cls, colors: bool = _has_colors, force_colors: bool = False
703+
) -> Styles:
704+
"""
705+
Configure and return the appropriate styles class for console output.
706+
707+
This method handles the setup of colorful or plain styles, including
708+
proper colorama initialization on Windows systems when colors are enabled.
709+
710+
Args:
711+
colors: Whether to use colorful output styles.
712+
713+
force_colors:
714+
Force colorful output even in non-interactive environments.
715+
Only relevant on Windows with colorama.
716+
717+
Returns:
718+
The configured styles class (_ColorfulStyles or _PlainStyles).
719+
720+
Raises:
721+
SystemError: On Windows when colors=True but colorama is not installed.
722+
723+
.. versionadded:: 25.5.0
724+
"""
725+
if not colors:
726+
return _PlainStyles
727+
728+
if _IS_WINDOWS: # pragma: no cover
729+
# On Windows, we can't do colorful output without colorama.
730+
if colorama is None:
731+
raise SystemError(
732+
_MISSING.format(
733+
who=cls.__name__ + " with `colors=True`",
734+
package="colorama",
735+
)
736+
)
737+
# Colorama must be init'd on Windows, but must NOT be
738+
# init'd on other OSes, because it can break colors.
739+
if force_colors:
740+
colorama.deinit()
741+
colorama.init(strip=False)
742+
else:
743+
colorama.init()
744+
745+
return _ColorfulStyles
746+
722747
def _repr(self, val: Any) -> str:
723748
"""
724749
Determine representation of *val* depending on its type &

tests/test_dev.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,64 @@ def test_init_accepts_overriding_levels(self, styles, padded):
175175
+ styles.reset
176176
) == rv
177177

178+
def test_returns_colorful_styles_when_colors_true(self):
179+
"""
180+
When colors=True, returns _ColorfulStyles class.
181+
"""
182+
styles = dev.ConsoleRenderer.get_default_column_styles(colors=True)
183+
184+
assert styles is dev._ColorfulStyles
185+
assert hasattr(styles, "reset")
186+
assert hasattr(styles, "bright")
187+
assert hasattr(styles, "level_critical")
188+
assert hasattr(styles, "kv_key")
189+
assert hasattr(styles, "kv_value")
190+
191+
def test_returns_plain_styles_when_colors_false(self):
192+
"""
193+
When colors=False, returns _PlainStyles class.
194+
"""
195+
styles = dev.ConsoleRenderer.get_default_column_styles(colors=False)
196+
197+
assert styles is dev._PlainStyles
198+
assert styles.reset == ""
199+
assert styles.bright == ""
200+
assert styles.level_critical == ""
201+
assert styles.kv_key == ""
202+
assert styles.kv_value == ""
203+
204+
@pytest.mark.skipif(
205+
not dev._IS_WINDOWS or dev.colorama is not None,
206+
reason="Only relevant on Windows without colorama",
207+
)
208+
def test_raises_system_error_on_windows_without_colorama(self):
209+
"""
210+
On Windows without colorama, raises SystemError when colors=True.
211+
"""
212+
with pytest.raises(SystemError, match="requires the colorama package"):
213+
dev.ConsoleRenderer.get_default_column_styles(colors=True)
214+
215+
@pytest.mark.skipif(
216+
not dev._IS_WINDOWS or dev.colorama is None,
217+
reason="Only relevant on Windows with colorama",
218+
)
219+
def test_initializes_colorama_on_windows_with_force_colors(self):
220+
"""
221+
On Windows with colorama, force_colors=True reinitializes colorama.
222+
"""
223+
with mock.patch.object(
224+
dev.colorama, "init"
225+
) as mock_init, mock.patch.object(
226+
dev.colorama, "deinit"
227+
) as mock_deinit:
228+
styles = dev.ConsoleRenderer.get_default_column_styles(
229+
colors=True, force_colors=True
230+
)
231+
232+
assert styles is dev._ColorfulStyles
233+
mock_deinit.assert_called_once()
234+
mock_init.assert_called_once_with(strip=False)
235+
178236
def test_logger_name(self, cr, styles, padded):
179237
"""
180238
Logger names are appended after the event.

0 commit comments

Comments
 (0)