Skip to content

Commit 1c828c4

Browse files
committed
feat(logging[extra]): add structured log calls with extra context across all modules
why: Structured `extra` keys (tmux_session, tmux_window, tmux_pane, tmux_config_path) enable filtering, aggregation, and test assertions on log records rather than string matching. what: - Add structured DEBUG/INFO/WARNING/ERROR log calls to workspace, CLI, and utility modules with appropriate extra keys - Route all raw print() calls through tmuxp_echo() in CLI commands - Fix get_pane() exception catch type to match sibling methods - Change before_script failure log from DEBUG to ERROR - Remove catch-log-reraise in plugin version check
1 parent 69a886c commit 1c828c4

17 files changed

Lines changed: 190 additions & 37 deletions

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/cli/convert.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import locale
6+
import logging
67
import os
78
import pathlib
89
import typing as t
@@ -13,7 +14,9 @@
1314
from tmuxp.workspace.finders import find_workspace_file, get_workspace_dir
1415

1516
from ._colors import Colors, build_description, get_color_mode
16-
from .utils import prompt_yes_no
17+
from .utils import prompt_yes_no, tmuxp_echo
18+
19+
logger = logging.getLogger(__name__)
1720

1821
CONVERT_DESCRIPTION = build_description(
1922
"""
@@ -130,7 +133,7 @@ def command_convert(
130133
new_workspace,
131134
encoding=locale.getpreferredencoding(False),
132135
)
133-
print( # NOQA: T201 RUF100
136+
tmuxp_echo(
134137
colors.success("New workspace file saved to ")
135138
+ colors.info(str(PrivatePath(newfile)))
136139
+ ".",

src/tmuxp/cli/edit.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import logging
56
import os
67
import subprocess
78
import typing as t
@@ -10,6 +11,9 @@
1011
from tmuxp.workspace.finders import find_workspace_file
1112

1213
from ._colors import Colors, build_description, get_color_mode
14+
from .utils import tmuxp_echo
15+
16+
logger = logging.getLogger(__name__)
1317

1418
EDIT_DESCRIPTION = build_description(
1519
"""
@@ -59,7 +63,7 @@ def command_edit(
5963
workspace_file = find_workspace_file(workspace_file)
6064

6165
sys_editor = os.environ.get("EDITOR", "vim")
62-
print( # NOQA: T201 RUF100
66+
tmuxp_echo(
6367
colors.muted("Opening ")
6468
+ colors.info(str(PrivatePath(workspace_file)))
6569
+ colors.muted(" in ")

src/tmuxp/cli/freeze.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import argparse
66
import locale
7+
import logging
78
import os
89
import pathlib
910
import sys
@@ -19,7 +20,9 @@
1920
from tmuxp.workspace.finders import get_workspace_dir
2021

2122
from ._colors import Colors, build_description, get_color_mode
22-
from .utils import prompt, prompt_choices, prompt_yes_no
23+
from .utils import prompt, prompt_choices, prompt_yes_no, tmuxp_echo
24+
25+
logger = logging.getLogger(__name__)
2326

2427
FREEZE_DESCRIPTION = build_description(
2528
"""
@@ -141,15 +144,15 @@ def command_freeze(
141144
if not session:
142145
raise exc.SessionNotFound
143146
except TmuxpException as e:
144-
print(colors.error(str(e))) # NOQA: T201 RUF100
147+
tmuxp_echo(colors.error(str(e)))
145148
return
146149

147150
frozen_workspace = freezer.freeze(session)
148151
workspace = freezer.inline(frozen_workspace)
149152
configparser = ConfigReader(workspace)
150153

151154
if not args.quiet:
152-
print( # NOQA: T201 RUF100
155+
tmuxp_echo(
153156
colors.format_separator(63)
154157
+ "\n"
155158
+ colors.muted("Freeze does its best to snapshot live tmux sessions.")
@@ -163,7 +166,7 @@ def command_freeze(
163166
)
164167
):
165168
if not args.quiet:
166-
print( # NOQA: T201 RUF100
169+
tmuxp_echo(
167170
colors.muted("tmuxp has examples in JSON and YAML format at ")
168171
+ colors.info("<http://tmuxp.git-pull.com/examples.html>")
169172
+ "\n"
@@ -190,7 +193,7 @@ def command_freeze(
190193
color_mode=color_mode,
191194
)
192195
if not args.force and os.path.exists(dest_prompt):
193-
print( # NOQA: T201 RUF100
196+
tmuxp_echo(
194197
colors.warning(f"{PrivatePath(dest_prompt)} exists.")
195198
+ " "
196199
+ colors.muted("Pick a new filename."),
@@ -252,8 +255,9 @@ def extract_workspace_format(
252255
workspace,
253256
encoding=locale.getpreferredencoding(False),
254257
)
258+
logger.info("workspace saved", extra={"tmux_config_path": str(dest)})
255259

256260
if not args.quiet:
257-
print( # NOQA: T201 RUF100
261+
tmuxp_echo(
258262
colors.success("Saved to ") + colors.info(str(PrivatePath(dest))) + ".",
259263
)

src/tmuxp/cli/import_config.py

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

55
import locale
6+
import logging
67
import os
78
import pathlib
89
import sys
@@ -16,6 +17,8 @@
1617
from ._colors import ColorMode, Colors, build_description, get_color_mode
1718
from .utils import prompt, prompt_choices, prompt_yes_no, tmuxp_echo
1819

20+
logger = logging.getLogger(__name__)
21+
1922
IMPORT_DESCRIPTION = build_description(
2023
"""
2124
Import workspaces from teamocil and tmuxinator configuration files.
@@ -220,6 +223,10 @@ def import_config(
220223
encoding=locale.getpreferredencoding(False),
221224
)
222225

226+
logger.info(
227+
"workspace saved",
228+
extra={"tmux_config_path": str(dest)},
229+
)
223230
tmuxp_echo(
224231
colors.success("Saved to ") + colors.info(str(PrivatePath(dest))) + ".",
225232
)

src/tmuxp/cli/load.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ def load_plugins(
123123
module_name = ".".join(module_name[:-1])
124124
plugin_name = plugin.split(".")[-1]
125125
except AttributeError as error:
126+
logger.exception("plugin load failed")
126127
tmuxp_echo(
127128
colors.error("[Plugin Error]")
128129
+ f" Couldn't load {plugin}\n"
@@ -139,12 +140,16 @@ def load_plugins(
139140
default=True,
140141
color_mode=colors.mode,
141142
):
143+
logger.warning(
144+
"plugin version constraint not met, user declined skip",
145+
)
142146
tmuxp_echo(
143147
colors.warning("[Not Skipping]")
144148
+ " Plugin versions constraint not met. Exiting...",
145149
)
146150
sys.exit(1)
147151
except (ImportError, AttributeError) as error:
152+
logger.exception("plugin import failed")
148153
tmuxp_echo(
149154
colors.error("[Plugin Error]")
150155
+ f" Couldn't load {plugin}\n"
@@ -178,7 +183,8 @@ def _reattach(builder: WorkspaceBuilder, colors: Colors | None = None) -> None:
178183
plugin.reattach(builder.session)
179184
proc = builder.session.cmd("display-message", "-p", "'#S'")
180185
for line in proc.stdout:
181-
print(colors.info(line) if colors else line) # NOQA: T201 RUF100
186+
tmuxp_echo(colors.info(line) if colors else line)
187+
logger.debug("reattach display-message output: %s", line.strip())
182188

183189
if "TMUX" in os.environ:
184190
builder.session.switch_client()
@@ -225,7 +231,8 @@ def _load_detached(builder: WorkspaceBuilder, colors: Colors | None = None) -> N
225231
assert builder.session is not None
226232

227233
msg = "Session created in detached state."
228-
print(colors.info(msg) if colors else msg) # NOQA: T201 RUF100
234+
tmuxp_echo(colors.info(msg) if colors else msg)
235+
logger.info("session created in detached state")
229236

230237

231238
def _load_append_windows_to_current_session(builder: WorkspaceBuilder) -> None:
@@ -347,10 +354,9 @@ def load_workspace(
347354
if isinstance(workspace_file, (str, os.PathLike)):
348355
workspace_file = pathlib.Path(workspace_file)
349356

350-
tmuxp_echo(
351-
cli_colors.info("[Loading]")
352-
+ " "
353-
+ cli_colors.highlight(str(PrivatePath(workspace_file))),
357+
logger.info(
358+
"loading workspace",
359+
extra={"tmux_config_path": str(workspace_file)},
354360
)
355361

356362
# ConfigReader allows us to open a yaml or json file as a dict
@@ -378,13 +384,18 @@ def load_workspace(
378384

379385
shutil.which("tmux") # raise exception if tmux not found
380386

381-
try: # load WorkspaceBuilder object for tmuxp workspace / tmux server
387+
# WorkspaceBuilder creation — outside spinner so plugin prompts are safe
388+
try:
382389
builder = WorkspaceBuilder(
383390
session_config=expanded_workspace,
384391
plugins=load_plugins(expanded_workspace, colors=cli_colors),
385392
server=t,
386393
)
387394
except exc.EmptyWorkspaceException:
395+
logger.warning(
396+
"workspace file is empty",
397+
extra={"tmux_config_path": str(workspace_file)},
398+
)
388399
tmuxp_echo(
389400
cli_colors.warning("[Warning]")
390401
+ f" {PrivatePath(workspace_file)} is empty or parsed no workspace data",
@@ -393,7 +404,7 @@ def load_workspace(
393404

394405
session_name = expanded_workspace["session_name"]
395406

396-
# if the session already exists, prompt the user to attach
407+
# Session-exists check — outside spinner so prompt_yes_no is safe
397408
if builder.session_exists(session_name) and not append:
398409
if not detached and (
399410
answer_yes
@@ -442,9 +453,7 @@ def load_workspace(
442453
_load_attached(builder, detached)
443454

444455
except exc.TmuxpException as e:
445-
import traceback
446-
447-
tmuxp_echo(traceback.format_exc())
456+
logger.exception("workspace build failed")
448457
tmuxp_echo(cli_colors.error("[Error]") + f" {e}")
449458

450459
choice = prompt_choices(
@@ -459,6 +468,7 @@ def load_workspace(
459468
if builder.session is not None:
460469
builder.session.kill()
461470
tmuxp_echo(cli_colors.muted("Session killed."))
471+
logger.info("session killed by user after build error")
462472
elif choice == "a":
463473
_reattach(builder, cli_colors)
464474
else:

src/tmuxp/cli/shell.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import argparse
6+
import logging
67
import os
78
import pathlib
89
import typing as t
@@ -13,6 +14,9 @@
1314
from tmuxp._compat import PY3, PYMINOR
1415

1516
from ._colors import Colors, build_description, get_color_mode
17+
from .utils import tmuxp_echo
18+
19+
logger = logging.getLogger(__name__)
1620

1721
SHELL_DESCRIPTION = build_description(
1822
"""
@@ -222,7 +226,7 @@ def command_shell(
222226
):
223227
from tmuxp._compat import breakpoint as tmuxp_breakpoint
224228

225-
print( # NOQA: T201 RUF100
229+
tmuxp_echo(
226230
cli_colors.muted("Launching ")
227231
+ cli_colors.highlight("pdb", bold=False)
228232
+ cli_colors.muted(" shell..."),
@@ -233,7 +237,7 @@ def command_shell(
233237
from tmuxp.shell import launch
234238

235239
shell_name = args.shell or "best"
236-
print( # NOQA: T201 RUF100
240+
tmuxp_echo(
237241
cli_colors.muted("Launching ")
238242
+ cli_colors.highlight(shell_name, bold=False)
239243
+ cli_colors.muted(" shell for session ")

src/tmuxp/cli/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import logging
56
import typing as t
67

78
from tmuxp._internal.colors import (
@@ -15,6 +16,8 @@
1516
from tmuxp._internal.private_path import PrivatePath
1617
from tmuxp.log import tmuxp_echo
1718

19+
logger = logging.getLogger(__name__)
20+
1821
if t.TYPE_CHECKING:
1922
from collections.abc import Callable, Sequence
2023

@@ -215,7 +218,7 @@ def prompt_choices(
215218
return None
216219
if rv in choices_:
217220
return rv
218-
print(
221+
tmuxp_echo(
219222
colors.warning(f"Invalid choice '{rv}'. ")
220223
+ f"Please choose from: {', '.join(choices_)}"
221224
)

src/tmuxp/plugin.py

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

33
from __future__ import annotations
44

5+
import logging
56
import typing as t
67

78
import libtmux
@@ -11,6 +12,8 @@
1112
from .__about__ import __version__
1213
from .exc import TmuxpPluginException
1314

15+
logger = logging.getLogger(__name__)
16+
1417
#: Minimum version of tmux required to run tmuxp
1518
TMUX_MIN_VERSION = "3.2"
1619

@@ -181,6 +184,7 @@ def __init__(self, **kwargs: Unpack[PluginConfigSchema]) -> None:
181184

182185
def _version_check(self) -> None:
183186
"""Check all dependency versions for compatibility."""
187+
logger.debug("checking version constraints for %s", self.plugin_name)
184188
for dep, constraints in self.version_constraints.items():
185189
assert isinstance(constraints, dict)
186190
try:

src/tmuxp/shell.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,17 @@ def has_bpython() -> bool:
106106
def detect_best_shell() -> CLIShellLiteral:
107107
"""Return the best, most feature-rich shell available."""
108108
if has_ptipython():
109-
return "ptipython"
110-
if has_ptpython():
111-
return "ptpython"
112-
if has_ipython():
113-
return "ipython"
114-
if has_bpython():
115-
return "bpython"
116-
return "code"
109+
shell: CLIShellLiteral = "ptipython"
110+
elif has_ptpython():
111+
shell = "ptpython"
112+
elif has_ipython():
113+
shell = "ipython"
114+
elif has_bpython():
115+
shell = "bpython"
116+
else:
117+
shell = "code"
118+
logger.debug("detected shell: %s", shell)
119+
return shell
117120

118121

119122
def get_bpython(

0 commit comments

Comments
 (0)