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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ jobs:
# Exercise the terminal initialization that we don't test otherwise.
- run: uv pip install colorama
if: runner.os == 'Windows'
- run: .\.venv\Scripts\python.exe -Ic 'import structlog; structlog.get_logger().info("test")'
- run: .\.venv\Scripts\python.exe -Ic 'import structlog; log = structlog.get_logger(); log.info("colors"); structlog.dev.ConsoleRenderer.get_active().colors = False; log.info("bw")'
if: runner.os == 'Windows'

required-checks-pass:
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
- Same for *columns*.
[#757](https://github.com/hynek/structlog/pull/757)

- Same for *colors* and *force_colors*.
[#759](https://github.com/hynek/structlog/pull/759)

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

Expand Down
4 changes: 4 additions & 0 deletions show_off.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class SomeClass:
log.warning("uh-uh!")
log.error("omg", a_dict={"a": 42, "b": "foo"})
log.critical("wtf", what=SomeClass(x=1, y="z"))
structlog.dev.ConsoleRenderer.get_active().colors = False
log.info("where are the colors!?", colors="gone")
structlog.dev.ConsoleRenderer.get_active().colors = True
log.info("there they are!", colors="back")


log2 = structlog.get_logger("another_logger")
Expand Down
52 changes: 52 additions & 0 deletions src/structlog/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,8 @@ def __init__(
self._sort_keys = sort_keys
self._repr_native_str = repr_native_str
self._styles = self.get_default_column_styles(colors, force_colors)
self._colors = colors
self._force_colors = force_colors
self._level_styles = (
self.get_default_level_styles(colors)
if level_styles is None
Expand Down Expand Up @@ -953,6 +955,56 @@ def columns(self, value: list[Column]) -> None:
self._default_column_formatter = defaults[0].formatter
self._columns = [col for col in value if col.key]

@property
def colors(self) -> bool:
"""
Whether to use colorful output styles.

Setting this will update the renderer's styles immediately and reset
level styles to the defaults according to the new color setting -- even
if the color value is the same.

.. versionadded:: 25.5.0
"""
return self._colors

@colors.setter
def colors(self, value: bool) -> None:
"""
.. versionadded:: 25.5.0
"""
self._colors = value
self._styles = self.get_default_column_styles(
value, self._force_colors
)
self._level_styles = self.get_default_level_styles(value)

self._configure_columns()

@property
def force_colors(self) -> bool:
"""
Force colorful output even in non-interactive environments.

Setting this will update the renderer's styles immediately and reset
level styles to the defaults according to the current color setting --
even if the value is the same.

.. versionadded:: 25.5.0
"""
return self._force_colors

@force_colors.setter
def force_colors(self, value: bool) -> None:
"""
.. versionadded:: 25.5.0
"""
self._force_colors = value
self._styles = self.get_default_column_styles(self._colors, value)
self._level_styles = self.get_default_level_styles(self._colors)

self._configure_columns()

@classmethod
def get_active(cls) -> ConsoleRenderer:
"""
Expand Down
143 changes: 143 additions & 0 deletions tests/test_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -899,3 +899,146 @@ def test_multiple_console_renderers(self):
structlog.exceptions.MultipleConsoleRenderersConfiguredError
):
dev.ConsoleRenderer.get_active()


class TestConsoleRendererColorsProperty:
def test_reset(self, cr):
"""
Setting colors to the same value resets the level styles to the
defaults.
"""
val = cr.colors
cr._level_styles = {"info": "X", "error": "Y"}

cr.colors = cr.colors

assert val is cr.colors
assert (
cr._level_styles
== dev.ConsoleRenderer.get_default_level_styles(colors=val)
)

@pytest.mark.skipif(
dev._IS_WINDOWS and dev.colorama is None,
reason="Toggling colors=True requires colorama on Windows",
)
def test_toggle_colors_updates_styles_and_levels(self):
"""
Toggling colors updates the styles and level styles to colorful styles.
"""
cr = dev.ConsoleRenderer(colors=False)

assert cr.colors is False
assert cr._colors is False
assert cr._styles is dev._plain_styles
assert (
cr._level_styles
== dev.ConsoleRenderer.get_default_level_styles(colors=False)
)

cr.colors = True

assert cr.colors is True
assert cr._colors is True
assert cr._styles is dev._colorful_styles
assert (
cr._level_styles
== dev.ConsoleRenderer.get_default_level_styles(colors=True)
)

@pytest.mark.skipif(
dev._IS_WINDOWS and dev.colorama is None,
reason="Toggling colors=True requires colorama on Windows",
)
def test_toggle_resets_custom_level_styles(self):
"""
Toggling colors resets the level styles to the defaults for the new
color setting.
"""
custom = {"info": "X", "error": "Y"}
cr = dev.ConsoleRenderer(colors=False, level_styles=custom)

assert custom == cr._level_styles

cr.colors = True
assert (
dev.ConsoleRenderer.get_default_level_styles(colors=True)
== cr._level_styles
)

# And switching back follows defaults for the new setting again
cr.colors = False
assert (
dev.ConsoleRenderer.get_default_level_styles(colors=False)
== cr._level_styles
)


class TestConsoleRendererForceColorsProperty:
def test_reset(self, cr):
"""
Setting force_colors to the same value resets the level styles to the
defaults.
"""
val = cr.force_colors

cr._level_styles = {"info": "X", "error": "Y"}

cr.force_colors = cr.force_colors

assert val is cr.force_colors
assert (
cr._level_styles
== dev.ConsoleRenderer.get_default_level_styles(colors=cr.colors)
)

def test_toggle_force_colors_updates_styles_and_levels(self):
"""
Setting force_colors to the same value resets the level styles to the
defaults.
"""
cr = dev.ConsoleRenderer(colors=True, force_colors=False)

assert cr.force_colors is False
assert cr._force_colors is False
assert cr._styles is dev.ConsoleRenderer.get_default_column_styles(
colors=True, force_colors=False
)
assert (
cr._level_styles
== dev.ConsoleRenderer.get_default_level_styles(colors=True)
)

cr.force_colors = True

assert cr.force_colors is True
assert cr._force_colors is True
assert cr._styles is dev.ConsoleRenderer.get_default_column_styles(
colors=True, force_colors=True
)
assert (
cr._level_styles
== dev.ConsoleRenderer.get_default_level_styles(colors=True)
)

def test_toggle_resets_custom_level_styles(self):
"""
Toggling force_colors resets the level styles to the defaults for the
new force_colors setting.
"""
custom = {"info": "X", "error": "Y"}
cr = dev.ConsoleRenderer(colors=True, level_styles=custom)

assert custom == cr._level_styles

cr.force_colors = True
assert (
dev.ConsoleRenderer.get_default_level_styles(colors=True)
== cr._level_styles
)

cr.force_colors = False
assert (
dev.ConsoleRenderer.get_default_level_styles(colors=True)
== cr._level_styles
)