10000 feat(logging) structured logging, colorama removal, OutputFormatter (… · tmux-python/tmuxp@13906cc · GitHub
[go: up one dir, main page]

Skip to content

Commit 13906cc

Browse files
authored
feat(logging) structured logging, colorama removal, OutputFormatter (#1017)
Add structured logging with `extra` context across all modules and clean up output channels. Every module now declares `logging.getLogger(__name__)` with structured keys (`tmux_session`, `tmux_window`, `tmux_pane`, `tmux_config_path`) for filtering and aggregation. A new `TmuxpLoggerAdapter` provides persistent identity context for session/window/pane objects, portable across Python 3.10–3.13+. - **Structured logging**: workspace builder emits INFO for session lifecycle and DEBUG for window/pane creation; finders, freezer, importers, loader, and validation modules emit DEBUG with config context; `NullHandler` added in library `__init__.py` - **Colorama removal**: replace `colorama` runtime and type-stub dependencies with stdlib ANSI escape constants - **Output channels**: route all raw `print()` calls through `tmuxp_echo()`; separate diagnostics (`logger.*()`) from user-facing output (`tmuxp_echo()`) per logging standards - **OutputFormatter.emit_object()**: single-object JSON output for `ls --json` and `debug-info --json`; `Colors.format_rule()` with Unicode box-drawing characters - **CLI log level**: default changed from INFO to WARNING so normal usage is not noisy - **Traceback suppression**: build-failure tracebacks moved from user-visible `tmuxp_echo(traceback.format_exc())` to `logger.debug(exc_info=True)`; users see `[Error] <message>`, full traceback available via `--log-level debug` - **`get_pane()` fix**: widen catch to `Exception` (matching `get_session`/`get_window`), preserve exception chain via `from e`, replace bare `print()` with structured debug log - **Log file handler**: `setup_log_file()` replaces inline handler setup in `command_load()`, respects `--log-level` flag Base PR for #1020 (animated progress spinner for `tmuxp load`).
2 parents 54aadae + 1f38d39 commit 13906cc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1067
-159
lines changed

AGENTS.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,24 @@ Assert on `caplog.records` attributes, not string matching on `caplog.text`:
206206
- Assert on schema: `record.tmux_exit_code == 0` not `"exit code 0" in caplog.text`
207207
- `caplog.record_tuples` cannot access extra fields — always use `caplog.records`
208208

209+
### Output channels
210+
211+
Two output channels serve different audiences:
212+
213+
1. **Diagnostics** (`logger.*()` with `extra`): System events for log files, `caplog`, and aggregators. Never styled.
214+
2. **User-facing output**: What the human sees. Styled via `Colors` class.
215+
- Commands with output modes (`--json`/`--ndjson`): prefer `OutputFormatter.emit_text()` from `tmuxp.cli._output` — silenced in non-human modes.
216+
- Human-only commands: use `tmuxp_echo()` from `tmuxp.log` (re-exported via `tmuxp.cli.utils`) for user-facing messages.
217+
- **Undefined contracts:** Machine-output behavior for error and empty-result paths (e.g., `search` with no matches) is not yet defined. These paths currently emit styled text through `formatter.emit_text()`, which is a no-op in machine modes.
218+
219+
Raw `print()` is forbidden in command/business logic. The `print()` call lives only inside the presenter layer (`_output.py`) or `tmuxp_echo`.
220+
209221
### Avoid
210222

211223
- f-strings/`.format()` in log calls
212224
- Unguarded logging in hot loops (guard with `isEnabledFor()`)
213225
- Catch-log-reraise without adding new context
214-
- `print()` for diagnostics
226+
- `print()` for debugging or internal diagnostics — use `logger.debug()` with structured `extra` instead
215227
- Logging secret env var values (log key names only)
216228
- Non-scalar ad-hoc objects in `extra`
217229
- Requiring custom `extra` fields in format strings without safe defaults (missing keys raise `KeyError`)

CHANGES

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,26 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force
3535
_Notes on the upcoming release will go here._
3636
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->
3737

38+
### Bug fixes
39+
40+
- Fix default CLI log level from INFO to WARNING so normal usage is not noisy (#1017)
41+
- Suppress raw Python tracebacks on workspace build failure; error details available via `--log-level debug` while the user sees only `[Error] <message>` (#1017)
42+
- Fix `get_pane()` to match sibling methods: widen catch to `Exception`, preserve exception chain via `from e`, replace bare `print()` with structured debug log (#1017)
43+
- Route `ls --json` and `debug-info --json` through `OutputFormatter` for consistent machine-readable output (#1017)
44+
45+
### Development
46+
47+
#### Structured logging with `extra` context across all modules (#1017)
48+
49+
All modules now use `logging.getLogger(__name__)` with structured `extra` keys
50+
(`tmux_session`, `tmux_window`, `tmux_pane`, `tmux_config_path`, etc.) for
51+
filtering and aggregation. Library `__init__.py` adds `NullHandler` per Python
52+
best practices. A new `TmuxpLoggerAdapter` provides persistent context for
53+
objects with stable identity.
54+
55+
- Remove `colorama` runtime and type-stub dependencies; replace with stdlib ANSI constants (#1017)
56+
- Route all raw `print()` calls through `tmuxp_echo()` for consistent output channels (#1017)
57+
3858
## tmuxp 1.65.0 (2026-03-08)
3959

4060
### Breaking Changes

pyproject.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ include = [
4040
]
4141
dependencies = [
4242
"libtmux~=0.55.0",
43-
"colorama>=0.3.9",
4443
"PyYAML>=6.0"
4544
]
4645

@@ -82,7 +81,6 @@ dev = [
8281
# Lint
8382
"ruff",
8483
"mypy",
85-
"types-colorama",
8684
"types-docutils",
8785
"types-Pygments",
8886
"types-PyYAML",
@@ -118,7 +116,6 @@ coverage =[
118116
lint = [
119117
"ruff",
120118
"mypy",
121-
"types-colorama",
122119
"types-docutils",
123120
"types-Pygments",
124121
"types-PyYAML",

src/tmuxp/__about__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
from __future__ import annotations
44

5+
import logging
6+
7+
logger = logging.getLogger(__name__)
8+
59
__title__ = "tmuxp"
610
__package_name__ = "tmuxp"
711
__version__ = "1.65.0"

src/tmuxp/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from __future__ import annotations
88

9+
import logging
10+
911
from . import cli, util
1012
from .__about__ import (
1113
__author__,
@@ -17,3 +19,6 @@
1719
__title__,
1820
__version__,
1921
)
22+
23+
logger = logging.getLogger(__name__)
24+
logger.addHandler(logging.NullHandler())

src/tmuxp/_compat.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,41 @@
1-
# flake8: NOQA
1+
from __future__ import annotations
2+
3+
import logging
24
import sys
35

6+
logger = logging.getLogger(__name__)
7+
48
PY3 = sys.version_info[0] == 3
59
PYMINOR = sys.version_info[1]
610
PYPATCH = sys.version_info[2]
711

8-
_identity = lambda x: x
12+
13+
def _identity(x: object) -> object:
14+
"""Return *x* unchanged — used as a no-op decorator.
15+
16+
Examples
17+
--------
18+
>>> from tmuxp._compat import _identity
19+
20+
Strings pass through unchanged:
21+
22+
>>> _identity("hello")
23+
'hello'
24+
25+
Integers pass through unchanged:
26+
27+
>>> _identity(42)
28+
42
29+
"""
30+
return x
31+
932

1033
if PY3 and PYMINOR >= 7:
11-
breakpoint = breakpoint
34+
breakpoint = breakpoint # noqa: A001
1235
else:
1336
import pdb
1437

15-
breakpoint = pdb.set_trace
38+
breakpoint = pdb.set_trace # noqa: A001
1639

1740

1841
implements_to_string = _identity

src/tmuxp/_internal/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
"""Internal APIs for tmuxp."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
7+
logger = logging.getLogger(__name__)

src/tmuxp/_internal/colors.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,14 @@
4343
from __future__ import annotations
4444

4545
import enum
46+
import logging
4647
import os
4748
import re
4849
import sys
4950
import typing as t
5051

52+
logger = logging.getLogger(__name__)
53+
5154
if t.TYPE_CHECKING:
5255
from typing import TypeAlias
5356

@@ -470,6 +473,33 @@ def format_separator(self, length: int = 25) -> str:
470473
"""
471474
return self.muted("-" * length)
472475

476+
def format_rule(self, width: int = 40, char: str = "─") -> str:
477+
"""Format a horizontal rule using Unicode box-drawing characters.
478+
479+
A richer alternative to ``format_separator()`` which uses plain hyphens.
480+
481+
Parameters
482+
----------
483+
width : int
484+
Number of characters. Default is 40.
485+
char : str
486+
Character to repeat. Default is ``"─"`` (U+2500).
487+
488+
Returns
489+
-------
490+
str
491+
Muted (blue) rule when colors enabled, plain rule otherwise.
492+
493+
Examples
494+
--------
495+
>>> colors = Colors(ColorMode.NEVER)
496+
>>> colors.format_rule(10)
497+
'──────────'
498+
>>> colors.format_rule(5, char="=")
499+
'====='
500+
"""
501+
return self.muted(char * width)
502+
473503
def format_kv(self, key: str, value: str) -> str:
474504
"""Format key: value pair with syntax highlighting.
475505

src/tmuxp/_internal/config_reader.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
from __future__ import annotations
44

55
import json
6+
import logging
67
import pathlib
78
import typing as t
89

910
import yaml
1011

12+
logger = logging.getLogger(__name__)
13+
1114
if t.TYPE_CHECKING:
1215
from typing import TypeAlias
1316

@@ -106,6 +109,7 @@ def _from_file(cls, path: pathlib.Path) -> dict[str, t.Any]:
106109
{'session_name': 'my session'}
107110
"""
108111
assert isinstance(path, pathlib.Path)
112+
logger.debug("loading config", extra={"tmux_config_path": str(path)})
109113
content = path.open(encoding="utf-8").read()
110114

111115
if path.suffix in {".yaml", ".yml"}:

src/tmuxp/_internal/private_path.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66

77
from __future__ import annotations
88

9+
import logging
910
import os
1011
import pathlib
1112
import typing as t
1213

14+
logger = logging.getLogger(__name__)
15+
1316
if t.TYPE_CHECKING:
1417
PrivatePathBase = pathlib.Path
1518
else:

0 commit comments

Comments
 (0)
0