From e5bf115f0d48d25a92da50138180095c73fdbf5d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 1 Jul 2023 17:45:55 -0500 Subject: [PATCH 01/45] ci(ruff): Strengthen code quality linting --- pyproject.toml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 85e938d9dd8..8b95af45cc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -155,6 +155,32 @@ module = [ ] ignore_missing_imports = true +[tool.ruff] +target-version = "py37" +select = [ + "E", # pycodestyle + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "Q", # flake8-quotes + "PTH", # flake8-use-pathlib + "SIM", # flake8-simplify + "TRY", # Trycertatops + "PERF", # Perflint + "RUF" # Ruff-specific rules +] + +[tool.ruff.isort] +known-first-party = [ + "tmuxp" +] +combine-as-imports = true + +[tool.ruff.per-file-ignores] +"*/__init__.py" = ["F401"] + [build-system] requires = ["poetry_core>=1.0.0"] build-backend = "poetry.core.masonry.api" From 62eb72cc70ea9bcc446bb8cabfe8ed90aabd453f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 8 Jul 2023 07:53:54 -0500 Subject: [PATCH 02/45] fix(pytest[zshrc]): Fix skipif condition --- conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 5ee456980de..a488b61a387 100644 --- a/conftest.py +++ b/conftest.py @@ -24,7 +24,7 @@ USING_ZSH = "zsh" in os.getenv("SHELL", "") -@pytest.mark.skipif(USING_ZSH, reason="Using ZSH") +@pytest.mark.skipif(not USING_ZSH, reason="Using ZSH") @pytest.fixture(autouse=USING_ZSH, scope="session") def zshrc(user_path: pathlib.Path) -> pathlib.Path: """This quiets ZSH default message. From e10145e8cf5ee5d01f12ea3d54621a5ae4369dee Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 2 Sep 2023 11:02:19 -0500 Subject: [PATCH 03/45] fix(get_current_pane): Fix try/catch broke by RUF015 See also: https://beta.ruff.rs/docs/rules/unnecessary-iterable-allocation-for-first-element/ --- src/tmuxp/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tmuxp/util.py b/src/tmuxp/util.py index 90acacf05a7..a0c86df8c81 100644 --- a/src/tmuxp/util.py +++ b/src/tmuxp/util.py @@ -87,8 +87,8 @@ def get_current_pane(server: "Server") -> t.Optional["Pane"]: """Return Pane if one found in env""" if os.getenv("TMUX_PANE") is not None: try: - return [p for p in server.panes if p.pane_id == os.getenv("TMUX_PANE")][0] - except IndexError: + return next(p for p in server.panes if p.pane_id == os.getenv("TMUX_PANE")) + except StopIteration: pass return None From 53f03e818e7a3f80f7f41f00ffa72adb15cdb6ef Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 26 Aug 2023 06:06:48 -0500 Subject: [PATCH 04/45] chore(ruff): Run automated fixes (97 errors) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ruff --show-fixes --fix . - conftest.py: 1 × I001 (unsorted-imports) - docs/_ext/aafig.py: 5 × C408 (unnecessary-collection-call) 1 × RUF005 (collection-literal-concatenation) - docs/conf.py: 2 × C408 (unnecessary-collection-call) 1 × I001 (unsorted-imports) 1 × SIM108 (if-else-block-instead-of-if-exp) 1 × RUF100 (unused-noqa) - src/tmuxp/cli/debug_info.py: 1 × I001 (unsorted-imports) 1 × C417 (unnecessary-map) - src/tmuxp/cli/freeze.py: 1 × I001 (unsorted-imports) - src/tmuxp/cli/load.py: 1 × I001 (unsorted-imports) 1 × UP031 (printf-string-formatting) 1 × UP032 (f-string) - src/tmuxp/cli/shell.py: 1 × RUF100 (unused-noqa) - src/tmuxp/cli/utils.py: 1 × UP031 (printf-string-formatting) 1 × UP032 (f-string) - src/tmuxp/exc.py: 1 × UP032 (f-string) - src/tmuxp/shell.py: 4 × RUF100 (unused-noqa) - src/tmuxp/types.py: 1 × RUF100 (unused-noqa) - src/tmuxp/util.py: 2 × RUF100 (unused-noqa) 1 × RUF015 (unnecessary-iterable-allocation-for-first-element) 1 × TRY201 (verbose-raise) - src/tmuxp/workspace/builder.py: 3 × SIM401 (if-else-block-instead-of-dict-get) 1 × F841 (unused-variable) 1 × B006 (mutable-argument-default) 1 × RUF013 (implicit-optional) 1 × TRY201 (verbose-raise) - src/tmuxp/workspace/finders.py: 2 × B006 (mutable-argument-default) 1 × RUF013 (implicit-optional) - src/tmuxp/workspace/freezer.py: 1 × I001 (unsorted-imports) 1 × UP018 (native-literals) - src/tmuxp/workspace/importers.py: 2 × B007 (unused-loop-control-variable) 1 × SIM401 (if-else-block-instead-of-dict-get) - src/tmuxp/workspace/loader.py: 2 × SIM401 (if-else-block-instead-of-dict-get) 1 × UP018 (native-literals) 1 × SIM102 (collapsible-if) - src/tmuxp/workspace/validation.py: 1 × SIM102 (collapsible-if) - tests/cli/test_cli.py: 2 × I001 (unsorted-imports) 2 × SIM105 (suppressible-exception) 1 × PTH201 (path-constructor-current-directory) - tests/cli/test_convert.py: 2 × SIM105 (suppressible-exception) 1 × I001 (unsorted-imports) - tests/cli/test_freeze.py: 2 × I001 (unsorted-imports) 2 × SIM105 (suppressible-exception) 2 × RUF005 (collection-literal-concatenation) - tests/cli/test_import.py: 2 × SIM105 (suppressible-exception) 1 × I001 (unsorted-imports) - tests/cli/test_load.py: 5 × SIM105 (suppressible-exception) 2 × I001 (unsorted-imports) 1 × RUF005 (collection-literal-concatenation) 1 × UP032 (f-string) - tests/cli/test_ls.py: 1 × SIM105 (suppressible-exception) 1 × I001 (unsorted-imports) - tests/cli/test_shell.py: 3 × C408 (unnecessary-collection-call) 2 × I001 (unsorted-imports) 1 × SIM105 (suppressible-exception) - tests/fixtures/__init__.py: 1 × RUF100 (unused-noqa) - tests/fixtures/import_teamocil/__init__.py: 1 × RUF100 (unused-noqa) - tests/fixtures/import_tmuxinator/__init__.py: 1 × RUF100 (unused-noqa) - tests/fixtures/workspace/__init__.py: 1 × RUF100 (unused-noqa) - tests/tests/test_helpers.py: 1 × I001 (unsorted-imports) - tests/workspace/test_builder.py: 1 × I001 (unsorted-imports) 1 × SIM300 (yoda-conditions) 1 × SIM108 (if-else-block-instead-of-if-exp) 1 × UP030 (format-literals) 1 × UP032 (f-string) - tests/workspace/test_config.py: 2 × B007 (unused-loop-control-variable) --- conftest.py | 3 +- docs/_ext/aafig.py | 28 ++++++------ docs/conf.py | 13 +++--- src/tmuxp/cli/debug_info.py | 3 +- src/tmuxp/cli/freeze.py | 1 + src/tmuxp/cli/load.py | 12 +++-- src/tmuxp/cli/shell.py | 2 +- src/tmuxp/cli/utils.py | 2 +- src/tmuxp/exc.py | 8 ++-- src/tmuxp/shell.py | 8 ++-- src/tmuxp/types.py | 2 +- src/tmuxp/util.py | 6 +-- src/tmuxp/workspace/builder.py | 23 ++++------ src/tmuxp/workspace/finders.py | 12 ++--- src/tmuxp/workspace/freezer.py | 5 ++- src/tmuxp/workspace/importers.py | 9 ++-- src/tmuxp/workspace/loader.py | 17 +++----- src/tmuxp/workspace/validation.py | 5 +-- tests/cli/test_cli.py | 16 +++---- tests/cli/test_convert.py | 10 ++--- tests/cli/test_freeze.py | 15 +++---- tests/cli/test_import.py | 9 ++-- tests/cli/test_load.py | 40 +++++++---------- tests/cli/test_ls.py | 6 +-- tests/cli/test_shell.py | 46 ++++++++++---------- tests/fixtures/__init__.py | 2 +- tests/fixtures/import_teamocil/__init__.py | 2 +- tests/fixtures/import_tmuxinator/__init__.py | 2 +- tests/fixtures/workspace/__init__.py | 2 +- tests/tests/test_helpers.py | 1 - tests/workspace/test_builder.py | 13 +++--- tests/workspace/test_config.py | 2 +- 32 files changed, 140 insertions(+), 185 deletions(-) diff --git a/conftest.py b/conftest.py index a488b61a387..aa132bb30b3 100644 --- a/conftest.py +++ b/conftest.py @@ -10,10 +10,9 @@ import typing as t import pytest - from _pytest.doctest import DoctestItem - from libtmux.test import namer + from tests.fixtures import utils as test_utils from tmuxp.workspace.finders import get_workspace_dir diff --git a/docs/_ext/aafig.py b/docs/_ext/aafig.py index 2c5775043c0..70c8b343911 100644 --- a/docs/_ext/aafig.py +++ b/docs/_ext/aafig.py @@ -28,7 +28,7 @@ logger = logging.getLogger(__name__) -DEFAULT_FORMATS = dict(html="svg", latex="pdf", text=None) +DEFAULT_FORMATS = {"html": "svg", "latex": "pdf", "text": None} def merge_dict(dst, src): @@ -58,21 +58,21 @@ class AafigDirective(images.Image): has_content = True required_arguments = 0 - own_option_spec = dict( - line_width=float, - background=str, - foreground=str, - fill=str, - aspect=nonnegative_int, - textual=flag, - proportional=flag, - ) + own_option_spec = { + "line_width": float, + "background": str, + "foreground": str, + "fill": str, + "aspect": nonnegative_int, + "textual": flag, + "proportional": flag, + } option_spec = images.Image.option_spec.copy() option_spec.update(own_option_spec) def run(self): - aafig_options = dict() - own_options_keys = [self.own_option_spec.keys()] + ["scale"] + aafig_options = {} + own_options_keys = [self.own_option_spec.keys(), "scale"] for (k, v) in self.options.items(): if k in own_options_keys: # convert flags to booleans @@ -88,7 +88,7 @@ def run(self): if isinstance(image_node, nodes.system_message): return [image_node] text = "\n".join(self.content) - image_node.aafig = dict(options=aafig_options, text=text) + image_node.aafig = {"options": aafig_options, "text": text} return [image_node] @@ -206,7 +206,7 @@ def setup(app): app.add_directive("aafig", AafigDirective) app.connect("doctree-read", render_aafig_images) app.add_config_value("aafig_format", DEFAULT_FORMATS, "html") - app.add_config_value("aafig_default_options", dict(), "html") + app.add_config_value("aafig_default_options", {}, "html") # vim: set expandtab shiftwidth=4 softtabstop=4 : diff --git a/docs/conf.py b/docs/conf.py index 5bf2406606b..fb81f6f9c4e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,10 +1,10 @@ # flake8: NOQA: E501 import contextlib import inspect +import pathlib import sys import typing as t from os.path import relpath -import pathlib import tmuxp @@ -172,12 +172,12 @@ } # aafig format, try to get working with pdf -aafig_format = dict(latex="pdf", html="gif") +aafig_format = {"latex": "pdf", "html": "gif"} -aafig_default_options = dict(scale=0.75, aspect=0.5, proportional=True) +aafig_default_options = {"scale": 0.75, "aspect": 0.5, "proportional": True} -def linkcode_resolve(domain, info): # NOQA: C901 +def linkcode_resolve(domain, info): """ Determine the URL corresponding to Python object @@ -224,10 +224,7 @@ def linkcode_resolve(domain, info): # NOQA: C901 except Exception: lineno = None - if lineno: - linespec = "#L%d-L%d" % (lineno, lineno + len(source) - 1) - else: - linespec = "" + linespec = "#L%d-L%d" % (lineno, lineno + len(source) - 1) if lineno else "" fn = relpath(fn, start=pathlib.Path(tmuxp.__file__).parent) diff --git a/src/tmuxp/cli/debug_info.py b/src/tmuxp/cli/debug_info.py index a23fac1f88f..d72e3fc1ac6 100644 --- a/src/tmuxp/cli/debug_info.py +++ b/src/tmuxp/cli/debug_info.py @@ -7,7 +7,6 @@ import typing as t from colorama import Fore - from libtmux.__about__ import __version__ as libtmux_version from libtmux.common import get_version, tmux_cmd @@ -34,7 +33,7 @@ def prepend_tab(strings): """ Prepend tab to strings in list. """ - return list(map(lambda x: "\t%s" % x, strings)) + return ["\t%s" % x for x in strings] def output_break(): """ diff --git a/src/tmuxp/cli/freeze.py b/src/tmuxp/cli/freeze.py index 2383a435ba5..bd0cf1dac4d 100644 --- a/src/tmuxp/cli/freeze.py +++ b/src/tmuxp/cli/freeze.py @@ -5,6 +5,7 @@ import typing as t from libtmux.server import Server + from tmuxp.config_reader import ConfigReader from tmuxp.exc import TmuxpException from tmuxp.workspace.finders import get_workspace_dir diff --git a/src/tmuxp/cli/load.py b/src/tmuxp/cli/load.py index 46862bb4238..8ad5c475427 100644 --- a/src/tmuxp/cli/load.py +++ b/src/tmuxp/cli/load.py @@ -16,6 +16,7 @@ from libtmux.common import has_gte_version from libtmux.server import Server from libtmux.session import Session + from tmuxp.types import StrPath from .. import config_reader, exc, log, util @@ -90,11 +91,7 @@ def set_layout_hook(session: Session, hook_name: str) -> None: hook_cmd.append("selectw -p") # unset the hook immediately after executing - hook_cmd.append( - "set-hook -u -t {target_session} {hook_name}".format( - target_session=session.id, hook_name=hook_name - ) - ) + hook_cmd.append(f"set-hook -u -t {session.id} {hook_name}") hook_cmd.append(f"selectw -t {attached_window.id}") # join the hook's commands with semicolons @@ -122,8 +119,9 @@ def load_plugins(session_config: t.Dict[str, t.Any]) -> t.List[t.Any]: plugins.append(plugin()) except exc.TmuxpPluginException as error: if not prompt_yes_no( - "%sSkip loading %s?" - % (style(str(error), fg="yellow"), plugin_name), + "{}Skip loading {}?".format( + style(str(error), fg="yellow"), plugin_name + ), default=True, ): tmuxp_echo( diff --git a/src/tmuxp/cli/shell.py b/src/tmuxp/cli/shell.py index 7ea117cbf20..d0774b2dffa 100644 --- a/src/tmuxp/cli/shell.py +++ b/src/tmuxp/cli/shell.py @@ -166,7 +166,7 @@ def command_shell( session=session, window_name=args.window_name, current_pane=current_pane ) - pane = util.get_pane(window=window, current_pane=current_pane) # NOQA: F841 + pane = util.get_pane(window=window, current_pane=current_pane) if args.command is not None: exec(args.command) diff --git a/src/tmuxp/cli/utils.py b/src/tmuxp/cli/utils.py index 9a2d41df9c4..b38ca689490 100644 --- a/src/tmuxp/cli/utils.py +++ b/src/tmuxp/cli/utils.py @@ -126,7 +126,7 @@ def prompt_choices( if isinstance(choice, str): options.append(choice) elif isinstance(choice, tuple): - options.append("%s [%s]" % (choice, choice[0])) + options.append(f"{choice} [{choice[0]}]") choice = choice[0] _choices.append(choice) diff --git a/src/tmuxp/exc.py b/src/tmuxp/exc.py index e8801e19f0c..beb9bbc5431 100644 --- a/src/tmuxp/exc.py +++ b/src/tmuxp/exc.py @@ -50,11 +50,11 @@ def __init__( self.cmd = cmd self.output = output self.message = ( - "before_script failed with returncode {returncode}.\n" - "command: {cmd}\n" + f"before_script failed with returncode {self.returncode}.\n" + f"command: {self.cmd}\n" "Error output:\n" - "{output}" - ).format(returncode=self.returncode, cmd=self.cmd, output=self.output) + f"{self.output}" + ) def __str__(self): return self.message diff --git a/src/tmuxp/shell.py b/src/tmuxp/shell.py index 44aa6fe55e1..378e8b9755d 100644 --- a/src/tmuxp/shell.py +++ b/src/tmuxp/shell.py @@ -32,7 +32,7 @@ def has_ipython() -> bool: def has_ptpython() -> bool: try: - from ptpython.repl import embed, run_config # NOQA F841 + from ptpython.repl import embed, run_config # F841 except ImportError: try: from prompt_toolkit.contrib.repl import embed, run_config # NOQA F841 @@ -44,8 +44,8 @@ def has_ptpython() -> bool: def has_ptipython() -> bool: try: - from ptpython.ipython import embed # NOQA F841 - from ptpython.repl import run_config # NOQA F841 + from ptpython.ipython import embed # F841 + from ptpython.repl import run_config # F841 except ImportError: try: from prompt_toolkit.contrib.ipython import embed # NOQA F841 @@ -80,7 +80,7 @@ def get_bpython(options, extra_args=None): if extra_args is None: extra_args = {} - from bpython import embed # NOQA F841 + from bpython import embed # F841 def launch_bpython(): imported_objects = get_launch_args(**options) diff --git a/src/tmuxp/types.py b/src/tmuxp/types.py index 536225c9f1d..f1f6c138f88 100644 --- a/src/tmuxp/types.py +++ b/src/tmuxp/types.py @@ -6,7 +6,7 @@ :class:`StrPath` and :class:`StrOrBytesPath` is based on `typeshed's`_. .. _typeshed's: https://github.com/python/typeshed/blob/9687d5/stdlib/_typeshed/__init__.pyi#L98 -""" # NOQA E501 +""" # E501 from os import PathLike from typing import Union diff --git a/src/tmuxp/util.py b/src/tmuxp/util.py index a0c86df8c81..3d313a50c4e 100644 --- a/src/tmuxp/util.py +++ b/src/tmuxp/util.py @@ -58,7 +58,7 @@ def run_before_script( if e.errno == 2: raise exc.BeforeLoadScriptNotExists(e, os.path.abspath(script_file)) else: - raise e + raise def oh_my_zsh_auto_title() -> None: @@ -151,9 +151,9 @@ def get_pane(window: "Window", current_pane: t.Optional["Pane"] = None) -> "Pane pane = None try: if current_pane is not None: - pane = window.panes.get(pane_id=current_pane.pane_id) # NOQA: F841 + pane = window.panes.get(pane_id=current_pane.pane_id) else: - pane = window.attached_pane # NOQA: F841 + pane = window.attached_pane except exc.TmuxpException as e: print(e) diff --git a/src/tmuxp/workspace/builder.py b/src/tmuxp/workspace/builder.py index efd2f2fc6aa..61b5c937bdc 100644 --- a/src/tmuxp/workspace/builder.py +++ b/src/tmuxp/workspace/builder.py @@ -141,7 +141,7 @@ def __init__( self, session_config: t.Dict[str, t.Any], server: Server, - plugins: t.List[t.Any] = [], + plugins: t.Optional[t.List[t.Any]] = None, ) -> None: """Initialize workspace loading. @@ -162,6 +162,8 @@ def __init__( ``self.session``. """ + if plugins is None: + plugins = [] if not session_config: raise exc.EmptyWorkspaceException("Session configuration is empty.") @@ -276,9 +278,9 @@ def build(self, session: t.Optional[Session] = None, append: bool = False) -> No if "start_directory" in self.session_config: cwd = self.session_config["start_directory"] run_before_script(self.session_config["before_script"], cwd=cwd) - except Exception as e: + except Exception: self.session.kill_session() - raise e + raise if "options" in self.session_config: for option, value in self.session_config["options"].items(): @@ -349,10 +351,7 @@ def iter_create_windows( for window_iterator, window_config in enumerate( self.session_config["windows"], start=1 ): - if "window_name" not in window_config: - window_name = None - else: - window_name = window_config["window_name"] + window_name = window_config.get("window_name", None) is_first_window_pass = self.first_window_pass( window_iterator, session, append @@ -363,20 +362,14 @@ def iter_create_windows( w1 = session.attached_window w1.move_window("99") - if "start_directory" in window_config: - start_directory = window_config["start_directory"] - else: - start_directory = None + start_directory = window_config.get("start_directory", None) # If the first pane specifies a start_directory, use that instead. panes = window_config["panes"] if panes and "start_directory" in panes[0]: start_directory = panes[0]["start_directory"] - if "window_shell" in window_config: - window_shell = window_config["window_shell"] - else: - window_shell = None + window_shell = window_config.get("window_shell", None) # If the first pane specifies a shell, use that instead. try: diff --git a/src/tmuxp/workspace/finders.py b/src/tmuxp/workspace/finders.py index 943de30bff6..76f2855c701 100644 --- a/src/tmuxp/workspace/finders.py +++ b/src/tmuxp/workspace/finders.py @@ -19,11 +19,7 @@ def is_workspace_file( filename: str, - extensions: t.Union["ValidExtensions", t.List["ValidExtensions"]] = [ - ".yml", - ".yaml", - ".json", - ], + extensions: t.Union["ValidExtensions", t.List["ValidExtensions"]] = None, ) -> bool: """ Return True if file has a valid workspace file type. @@ -39,13 +35,15 @@ def is_workspace_file( ------- bool """ + if extensions is None: + extensions = [".yml", ".yaml", ".json"] extensions = [extensions] if isinstance(extensions, str) else extensions return any(filename.endswith(e) for e in extensions) def in_dir( workspace_dir: t.Union[pathlib.Path, str] = os.path.expanduser("~/.tmuxp"), - extensions: t.List["ValidExtensions"] = [".yml", ".yaml", ".json"], + extensions: t.Optional[t.List["ValidExtensions"]] = None, ) -> t.List[str]: """ Return a list of workspace_files in ``workspace_dir``. @@ -61,6 +59,8 @@ def in_dir( ------- list """ + if extensions is None: + extensions = [".yml", ".yaml", ".json"] workspace_files = [] for filename in os.listdir(workspace_dir): diff --git a/src/tmuxp/workspace/freezer.py b/src/tmuxp/workspace/freezer.py index b9b4ab9ddb3..ab29b36caaf 100644 --- a/src/tmuxp/workspace/freezer.py +++ b/src/tmuxp/workspace/freezer.py @@ -1,6 +1,7 @@ +import typing as t + from libtmux.pane import Pane from libtmux.session import Session -import typing as t def inline(workspace_dict): @@ -23,7 +24,7 @@ def inline(workspace_dict): ): workspace_dict["shell_command"] = workspace_dict["shell_command"][0] - if len(workspace_dict.keys()) == int(1): + if len(workspace_dict.keys()) == 1: workspace_dict = workspace_dict["shell_command"] if ( "shell_command_before" in workspace_dict diff --git a/src/tmuxp/workspace/importers.py b/src/tmuxp/workspace/importers.py index b5eff6606ba..9f60208f28c 100644 --- a/src/tmuxp/workspace/importers.py +++ b/src/tmuxp/workspace/importers.py @@ -126,10 +126,7 @@ def import_teamocil(workspace_dict: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: if "session" in workspace_dict: workspace_dict = workspace_dict["session"] - if "name" in workspace_dict: - tmuxp_workspace["session_name"] = workspace_dict["name"] - else: - tmuxp_workspace["session_name"] = None + tmuxp_workspace["session_name"] = workspace_dict.get("name", None) if "root" in workspace_dict: tmuxp_workspace["start_directory"] = workspace_dict.pop("root") @@ -144,10 +141,10 @@ def import_teamocil(workspace_dict: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: if "filters" in w: if "before" in w["filters"]: - for b in w["filters"]["before"]: + for _b in w["filters"]["before"]: window_dict["shell_command_before"] = w["filters"]["before"] if "after" in w["filters"]: - for b in w["filters"]["after"]: + for _b in w["filters"]["after"]: window_dict["shell_command_after"] = w["filters"]["after"] if "root" in w: diff --git a/src/tmuxp/workspace/loader.py b/src/tmuxp/workspace/loader.py index 9fc30330b3a..dcf1ef090a8 100644 --- a/src/tmuxp/workspace/loader.py +++ b/src/tmuxp/workspace/loader.py @@ -48,7 +48,7 @@ def expand_cmd(p: Dict) -> Dict: if not cmds or any(a == cmds for a in [None, "blank", "pane"]): cmds = [] - if isinstance(cmds, list) and len(cmds) == int(1): + if isinstance(cmds, list) and len(cmds) == 1: if any(a in cmds for a in [None, "blank", "pane"]): cmds = [] @@ -206,15 +206,9 @@ def trickle(workspace_dict): # prepends a pane's ``shell_command`` list with the window and sessions' # ``shell_command_before``. - if "start_directory" in workspace_dict: - session_start_directory = workspace_dict["start_directory"] - else: - session_start_directory = None + session_start_directory = workspace_dict.get("start_directory", None) - if "suppress_history" in workspace_dict: - suppress_history = workspace_dict["suppress_history"] - else: - suppress_history = None + suppress_history = workspace_dict.get("suppress_history", None) for window_dict in workspace_dict["windows"]: # Prepend start_directory to relative window commands @@ -231,9 +225,8 @@ def trickle(workspace_dict): window_dict["start_directory"] = window_start_path # We only need to trickle to the window, workspace builder checks wconf - if suppress_history is not None: - if "suppress_history" not in window_dict: - window_dict["suppress_history"] = suppress_history + if suppress_history is not None and "suppress_history" not in window_dict: + window_dict["suppress_history"] = suppress_history # If panes were NOT specified for a window, assume that a single pane # with no shell commands is desired diff --git a/src/tmuxp/workspace/validation.py b/src/tmuxp/workspace/validation.py index bbe1ede7f9e..507ce64c7e5 100644 --- a/src/tmuxp/workspace/validation.py +++ b/src/tmuxp/workspace/validation.py @@ -27,8 +27,7 @@ def validate_schema(workspace_dict: t.Any) -> bool: if "window_name" not in window: raise exc.WorkspaceError('workspace window is missing "window_name"') - if "plugins" in workspace_dict: - if not isinstance(workspace_dict["plugins"], list): - raise exc.WorkspaceError('"plugins" only supports list type') + if "plugins" in workspace_dict and not isinstance(workspace_dict["plugins"], list): + raise exc.WorkspaceError('"plugins" only supports list type') return True diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index eacf8506973..f67234279f9 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1,12 +1,13 @@ import argparse +import contextlib import os import pathlib import typing as t -import pytest - import libtmux +import pytest from libtmux.server import Server + from tmuxp import cli from tmuxp.cli.import_config import get_teamocil_dir, get_tmuxinator_dir from tmuxp.cli.load import _reattach, load_plugins @@ -42,10 +43,9 @@ def test_help( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture, ) -> None: - try: + with contextlib.suppress(SystemExit): cli.cli(cli_args) - except SystemExit: - pass + result = capsys.readouterr() assert "usage: tmuxp [-h] [--version] [--log-level log-level]" in result.out @@ -57,7 +57,7 @@ def test_resolve_behavior( expect = tmp_path monkeypatch.chdir(tmp_path) assert pathlib.Path("../").resolve() == pathlib.Path(os.path.dirname(expect)) - assert pathlib.Path(".").resolve() == expect + assert pathlib.Path().resolve() == expect assert pathlib.Path("./").resolve() == expect assert pathlib.Path(expect).resolve() == expect @@ -129,10 +129,8 @@ def test_reattach_plugins( ) builder.build() - try: + with contextlib.suppress(libtmux.exc.LibTmuxException): _reattach(builder) - except libtmux.exc.LibTmuxException: - pass assert builder.session is not None proc = builder.session.cmd("display-message", "-p", "'#S'") diff --git a/tests/cli/test_convert.py b/tests/cli/test_convert.py index b923c4ea1d2..6a36e3e46d6 100644 --- a/tests/cli/test_convert.py +++ b/tests/cli/test_convert.py @@ -1,3 +1,4 @@ +import contextlib import io import json import pathlib @@ -41,10 +42,9 @@ def test_convert( input_args = "y\ny\n" if "-y" not in cli_args else "" monkeypatch.setattr("sys.stdin", io.StringIO(input_args)) - try: + with contextlib.suppress(SystemExit): cli.cli(cli_args) - except SystemExit: - pass + tmuxp_json = tmp_path / ".tmuxp.json" assert tmuxp_json.exists() assert tmuxp_json.open().read() == json.dumps({"session_name": "hello"}, indent=2) @@ -76,10 +76,8 @@ def test_convert_json( input_args = "y\ny\n" if "-y" not in cli_args else "" monkeypatch.setattr("sys.stdin", io.StringIO(input_args)) - try: + with contextlib.suppress(SystemExit): cli.cli(cli_args) - except SystemExit: - pass tmuxp_yaml = tmp_path / ".tmuxp.yaml" assert tmuxp_yaml.exists() diff --git a/tests/cli/test_freeze.py b/tests/cli/test_freeze.py index 87ddc58c143..9d5fac36e2a 100644 --- a/tests/cli/test_freeze.py +++ b/tests/cli/test_freeze.py @@ -1,10 +1,11 @@ +import contextlib import io import pathlib import typing as t import pytest - from libtmux.server import Server + from tmuxp import cli from tmuxp.config_reader import ConfigReader @@ -47,13 +48,11 @@ def test_freeze( monkeypatch.chdir(tmp_path) # Use tmux server (socket name) used in the test assert server.socket_name is not None - cli_args = cli_args + ["-L", server.socket_name] + cli_args = [*cli_args, "-L", server.socket_name] monkeypatch.setattr("sys.stdin", io.StringIO("".join(inputs))) - try: + with contextlib.suppress(SystemExit): cli.cli(cli_args) - except SystemExit: - pass yaml_config_path = tmp_path / "la.yaml" assert yaml_config_path.exists() @@ -93,13 +92,11 @@ def test_freeze_overwrite( monkeypatch.chdir(tmp_path) # Use tmux server (socket name) used in the test assert server.socket_name is not None - cli_args = cli_args + ["-L", server.socket_name] + cli_args = [*cli_args, "-L", server.socket_name] monkeypatch.setattr("sys.stdin", io.StringIO("".join(inputs))) - try: + with contextlib.suppress(SystemExit): cli.cli(cli_args) - except SystemExit: - pass yaml_config_path = tmp_path / "exists.yaml" assert yaml_config_path.exists() diff --git a/tests/cli/test_import.py b/tests/cli/test_import.py index 0d1e76a9220..ac8c295a4d9 100644 --- a/tests/cli/test_import.py +++ b/tests/cli/test_import.py @@ -1,3 +1,4 @@ +import contextlib import io import pathlib import typing as t @@ -61,10 +62,8 @@ def test_import_teamocil( monkeypatch.chdir(tmp_path) monkeypatch.setattr("sys.stdin", io.StringIO("".join(inputs))) - try: + with contextlib.suppress(SystemExit): cli.cli(cli_args) - except SystemExit: - pass new_config_yaml = tmp_path / "la.yaml" assert new_config_yaml.exists() @@ -109,10 +108,8 @@ def test_import_tmuxinator( monkeypatch.chdir(tmp_path) monkeypatch.setattr("sys.stdin", io.StringIO("".join(inputs))) - try: + with contextlib.suppress(SystemExit): cli.cli(cli_args) - except SystemExit: - pass new_config_yaml = tmp_path / "la.yaml" assert new_config_yaml.exists() diff --git a/tests/cli/test_load.py b/tests/cli/test_load.py index 6a42ce1ef75..aa341245ab3 100644 --- a/tests/cli/test_load.py +++ b/tests/cli/test_load.py @@ -1,15 +1,15 @@ +import contextlib import io import pathlib import typing as t -import pytest - -from pytest_mock import MockerFixture - import libtmux +import pytest from libtmux.common import has_lt_version from libtmux.server import Server from libtmux.session import Session +from pytest_mock import MockerFixture + from tmuxp import cli from tmuxp.cli.load import ( _load_append_windows_to_current_session, @@ -282,22 +282,18 @@ def test_load( config_path.format(tmp_path=tmp_path, TMUXP_CONFIGDIR=tmuxp_configdir) ) tmuxp_config.write_text( - """ + f""" session_name: {session_name} windows: - window_name: test panes: - - """.format( - session_name=session_name - ), + """, encoding="utf-8", ) - try: + with contextlib.suppress(SystemExit): cli.cli([*cli_args, "-d", "-L", server.socket_name, "-y"]) - except SystemExit: - pass result = capsys.readouterr() output = "".join(list(result.out)) @@ -363,7 +359,7 @@ def test_load_zsh_autotitle_warning( # Use tmux server (socket name) used in the test assert server.socket_name is not None - cli_args = cli_args + ["-L", server.socket_name] + cli_args = [*cli_args, "-L", server.socket_name] cli.cli(cli_args) result = capsys.readouterr() @@ -413,10 +409,9 @@ def test_load_log_file( monkeypatch.chdir(tmp_path) - try: + with contextlib.suppress(Exception): cli.cli(cli_args) - except Exception: - pass + result = capsys.readouterr() log_file_path = tmp_path / "log.txt" assert "Loading" in log_file_path.open().read() @@ -457,10 +452,9 @@ def test_load_plugins(monkeypatch_plugin_test_packages: None) -> None: def test_load_plugins_version_fail_skip( monkeypatch_plugin_test_packages, cli_args, inputs, capsys: pytest.CaptureFixture ) -> None: - try: + with contextlib.suppress(SystemExit): cli.cli(cli_args) - except SystemExit: - pass + result = capsys.readouterr() assert "[Loading]" in result.out @@ -484,10 +478,9 @@ def test_load_plugins_version_fail_no_skip( ) -> None: monkeypatch.setattr("sys.stdin", io.StringIO("".join(inputs))) - try: + with contextlib.suppress(SystemExit): cli.cli(cli_args) - except SystemExit: - pass + result = capsys.readouterr() assert "[Not Skipping]" in result.out @@ -502,10 +495,9 @@ def test_load_plugins_plugin_missing( cli_args: t.List[str], capsys: pytest.CaptureFixture, ) -> None: - try: + with contextlib.suppress(SystemExit): cli.cli(cli_args) - except SystemExit: - pass + result = capsys.readouterr() assert "[Plugin Error]" in result.out diff --git a/tests/cli/test_ls.py b/tests/cli/test_ls.py index 75d6de7db54..fa3c08d0d00 100644 --- a/tests/cli/test_ls.py +++ b/tests/cli/test_ls.py @@ -1,3 +1,4 @@ +import contextlib import os import pathlib @@ -36,10 +37,9 @@ def test_ls_cli( else: location.touch() - try: + with contextlib.suppress(SystemExit): cli.cli(["ls"]) - except SystemExit: - pass + cli_output = capsys.readouterr().out assert cli_output == "\n".join(stems) + "\n" diff --git a/tests/cli/test_shell.py b/tests/cli/test_shell.py index 8dafac33462..c95d918c919 100644 --- a/tests/cli/test_shell.py +++ b/tests/cli/test_shell.py @@ -1,12 +1,13 @@ +import contextlib import io import pathlib import subprocess import typing as t import pytest - from libtmux.server import Server from libtmux.session import Session + from tmuxp import cli, exc @@ -98,13 +99,13 @@ def test_shell( assert window.attached_pane is not None - template_ctx = dict( - SOCKET_NAME=server.socket_name, - SESSION_NAME=session.name, - WINDOW_NAME=window_name, - PANE_ID=window.attached_pane.id, - SERVER_SOCKET_NAME=server.socket_name, - ) + template_ctx = { + "SOCKET_NAME": server.socket_name, + "SESSION_NAME": session.name, + "WINDOW_NAME": window_name, + "PANE_ID": window.attached_pane.id, + "SERVER_SOCKET_NAME": server.socket_name, + } cli_args = cli_cmd + [cli_arg.format(**template_ctx) for cli_arg in cli_args] @@ -191,11 +192,11 @@ def test_shell_target_missing( assert session.name is not None template_ctx.update( - dict( - SOCKET_NAME=server.socket_name, - SESSION_NAME=session.name, - WINDOW_NAME=template_ctx.get("window_name", window_name), - ) + { + "SOCKET_NAME": server.socket_name, + "SESSION_NAME": session.name, + "WINDOW_NAME": template_ctx.get("window_name", window_name), + } ) cli_args = cli_cmd + [cli_arg.format(**template_ctx) for cli_arg in cli_args] @@ -265,13 +266,13 @@ def test_shell_interactive( assert window.attached_pane is not None - template_ctx = dict( - SOCKET_NAME=server.socket_name, - SESSION_NAME=session.name, - WINDOW_NAME=window_name, - PANE_ID=window.attached_pane.id, - SERVER_SOCKET_NAME=server.socket_name, - ) + template_ctx = { + "SOCKET_NAME": server.socket_name, + "SESSION_NAME": session.name, + "WINDOW_NAME": window_name, + "PANE_ID": window.attached_pane.id, + "SERVER_SOCKET_NAME": server.socket_name, + } cli_args = cli_cmd + [cli_arg.format(**template_ctx) for cli_arg in cli_args] @@ -280,9 +281,8 @@ def test_shell_interactive( monkeypatch.chdir(tmp_path) monkeypatch.setattr("sys.stdin", io.StringIO("exit()\r")) - try: + with contextlib.suppress(SystemExit): cli.cli(cli_args) - except SystemExit: - pass + result = capsys.readouterr() assert message.format(**template_ctx) in result.err diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py index a81b5f2914d..eb018c3ff09 100644 --- a/tests/fixtures/__init__.py +++ b/tests/fixtures/__init__.py @@ -1 +1 @@ -from . import utils # noqa +from . import utils diff --git a/tests/fixtures/import_teamocil/__init__.py b/tests/fixtures/import_teamocil/__init__.py index 7fe671395a8..6d5ce91af9e 100644 --- a/tests/fixtures/import_teamocil/__init__.py +++ b/tests/fixtures/import_teamocil/__init__.py @@ -1 +1 @@ -from . import layouts, test1, test2, test3, test4 # noqa +from . import layouts, test1, test2, test3, test4 diff --git a/tests/fixtures/import_tmuxinator/__init__.py b/tests/fixtures/import_tmuxinator/__init__.py index bbdbf699ad0..91bc1d0f43e 100644 --- a/tests/fixtures/import_tmuxinator/__init__.py +++ b/tests/fixtures/import_tmuxinator/__init__.py @@ -1 +1 @@ -from . import test1, test2, test3 # noqa +from . import test1, test2, test3 diff --git a/tests/fixtures/workspace/__init__.py b/tests/fixtures/workspace/__init__.py index 11692230916..097409c1569 100644 --- a/tests/fixtures/workspace/__init__.py +++ b/tests/fixtures/workspace/__init__.py @@ -1,4 +1,4 @@ -from . import ( # noqa +from . import ( expand1, expand2, expand_blank, diff --git a/tests/tests/test_helpers.py b/tests/tests/test_helpers.py index 0cb9ba21b83..5928f73937a 100644 --- a/tests/tests/test_helpers.py +++ b/tests/tests/test_helpers.py @@ -1,6 +1,5 @@ """Tests for .'s helper and utility functions.""" import pytest - from libtmux.server import Server from libtmux.test import get_test_session_name, temp_session diff --git a/tests/workspace/test_builder.py b/tests/workspace/test_builder.py index 30d0e85ade4..bb8a0f3c806 100644 --- a/tests/workspace/test_builder.py +++ b/tests/workspace/test_builder.py @@ -5,14 +5,14 @@ import time import typing as t -import pytest - import libtmux +import pytest from libtmux.common import has_gte_version, has_lt_version from libtmux.pane import Pane from libtmux.session import Session from libtmux.test import retry_until, temp_session from libtmux.window import Window + from tmuxp import exc from tmuxp.cli.load import load_plugins from tmuxp.config_reader import ConfigReader @@ -87,10 +87,7 @@ def test_focus_pane_index(session): assert isinstance(_pane_base_index, int) pane_base_index = int(_pane_base_index) - if not pane_base_index: - pane_base_index = 0 - else: - pane_base_index = int(pane_base_index) + pane_base_index = 0 if not pane_base_index else int(pane_base_index) # get the pane index for each pane pane_base_indexes = [] @@ -231,7 +228,7 @@ def test_global_options(session): _status_position = session.show_option("status-position", _global=True) assert isinstance(_status_position, str) assert "top" in _status_position - assert 493 == session.show_option("repeat-time", _global=True) + assert session.show_option("repeat-time", _global=True) == 493 def test_global_session_env_options(session, monkeypatch): @@ -603,7 +600,7 @@ def test_start_directory_sets_session_path(server): builder.build() session = builder.session - expected = "{0}|/usr".format(session.id) + expected = f"{session.id}|/usr" cmd = server.cmd("list-sessions", "-F", "#{session_id}|#{session_path}") assert expected in cmd.stdout diff --git a/tests/workspace/test_config.py b/tests/workspace/test_config.py index 4bc142b9ecb..fb21ef2ca1a 100644 --- a/tests/workspace/test_config.py +++ b/tests/workspace/test_config.py @@ -49,7 +49,7 @@ def test_find_workspace_file(tmp_path: pathlib.Path): garbage_file = tmp_path / "config.psd" garbage_file.write_text("wat", encoding="utf-8") - for r, d, f in os.walk(str(tmp_path)): + for _r, _d, f in os.walk(str(tmp_path)): for filela in (x for x in f if x.endswith((".json", ".ini", "yaml"))): configs.append(str(tmp_path / filela)) From 2d68c3789f5b4ab46434f0ed3332a6a980e1542c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Sep 2023 06:24:27 -0500 Subject: [PATCH 05/45] refactor(test_cli, get_{teamocil,tmuxinator}_dir): To pathlib --- src/tmuxp/cli/import_config.py | 14 +++++++------- tests/cli/test_cli.py | 19 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/tmuxp/cli/import_config.py b/src/tmuxp/cli/import_config.py index 895ee6f2141..b2296caded5 100644 --- a/src/tmuxp/cli/import_config.py +++ b/src/tmuxp/cli/import_config.py @@ -11,7 +11,7 @@ from .utils import prompt, prompt_choices, prompt_yes_no, tmuxp_echo -def get_tmuxinator_dir() -> str: +def get_tmuxinator_dir() -> pathlib.Path: """ Return tmuxinator configuration directory. @@ -19,7 +19,7 @@ def get_tmuxinator_dir() -> str: Returns ------- - str : + pathlib.Path : absolute path to tmuxinator config directory See Also @@ -27,25 +27,25 @@ def get_tmuxinator_dir() -> str: :meth:`tmuxp.workspace.importers.tmuxinator.import_tmuxinator` """ if "TMUXINATOR_CONFIG" in os.environ: - return os.path.expanduser(os.environ["TMUXINATOR_CONFIG"]) + return pathlib.Path(os.environ["TMUXINATOR_CONFIG"]).expanduser() - return os.path.expanduser("~/.tmuxinator/") + return pathlib.Path("~/.tmuxinator/").expanduser() -def get_teamocil_dir() -> str: +def get_teamocil_dir() -> pathlib.Path: """ Return teamocil configuration directory. Returns ------- - str : + pathlib.Path : absolute path to teamocil config directory See Also -------- :meth:`tmuxp.workspace.importers.teamocil.import_teamocil` """ - return os.path.expanduser("~/.teamocil/") + return pathlib.Path("~/.teamocil/").expanduser() def _resolve_path_no_overwrite(workspace_file: str) -> str: diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index f67234279f9..3d52bacc924 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1,6 +1,5 @@ import argparse import contextlib -import os import pathlib import typing as t @@ -27,7 +26,7 @@ def test_creates_config_dir_not_exists(tmp_path: pathlib.Path) -> None: """cli.startup() creates config dir if not exists.""" cli.startup(tmp_path) - assert os.path.exists(tmp_path) + assert tmp_path.exists() @pytest.mark.parametrize( @@ -56,26 +55,28 @@ def test_resolve_behavior( ) -> None: expect = tmp_path monkeypatch.chdir(tmp_path) - assert pathlib.Path("../").resolve() == pathlib.Path(os.path.dirname(expect)) + assert pathlib.Path("../").resolve() == expect.parent assert pathlib.Path().resolve() == expect assert pathlib.Path("./").resolve() == expect assert pathlib.Path(expect).resolve() == expect def test_get_tmuxinator_dir(monkeypatch: pytest.MonkeyPatch) -> None: - assert get_tmuxinator_dir() == os.path.expanduser("~/.tmuxinator/") + assert get_tmuxinator_dir() == pathlib.Path("~/.tmuxinator").expanduser() monkeypatch.setenv("HOME", "/moo") - assert get_tmuxinator_dir() == "/moo/.tmuxinator/" - assert get_tmuxinator_dir() == os.path.expanduser("~/.tmuxinator/") + assert get_tmuxinator_dir() == pathlib.Path("/moo/.tmuxinator/") + assert str(get_tmuxinator_dir()) == "/moo/.tmuxinator" + assert get_tmuxinator_dir() == pathlib.Path("~/.tmuxinator/").expanduser() def test_get_teamocil_dir(monkeypatch: pytest.MonkeyPatch) -> None: - assert get_teamocil_dir() == os.path.expanduser("~/.teamocil/") + assert get_teamocil_dir() == pathlib.Path("~/.teamocil/").expanduser() monkeypatch.setenv("HOME", "/moo") - assert get_teamocil_dir() == "/moo/.teamocil/" - assert get_teamocil_dir() == os.path.expanduser("~/.teamocil/") + assert get_teamocil_dir() == pathlib.Path("/moo/.teamocil/") + assert str(get_teamocil_dir()) == "/moo/.teamocil" + assert get_teamocil_dir() == pathlib.Path("~/.teamocil/").expanduser() def test_pass_config_dir_ClickPath( From e2b4b3682bb6033bf8316c91797e1fd229382e70 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Sep 2023 06:42:40 -0500 Subject: [PATCH 06/45] refactor(workspace.validation): Use dedicated exceptions --- src/tmuxp/workspace/validation.py | 50 ++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/tmuxp/workspace/validation.py b/src/tmuxp/workspace/validation.py index 507ce64c7e5..02578e1de80 100644 --- a/src/tmuxp/workspace/validation.py +++ b/src/tmuxp/workspace/validation.py @@ -3,6 +3,48 @@ from .. import exc +class SchemaValidationError(exc.WorkspaceError): + pass + + +class SessionNameMissingValidationError(SchemaValidationError): + def __init__(self, *args: object, **kwargs: object) -> None: + return super().__init__( + 'workspace requires "session_name"', + *args, + **kwargs, + ) + + +class WindowListMissingValidationError(SchemaValidationError): + def __init__(self, *args: object, **kwargs: object) -> None: + return super().__init__( + 'workspace requires list of "windows"', + *args, + **kwargs, + ) + + +class WindowNameMissingValidationError(SchemaValidationError): + def __init__(self, *args: object, **kwargs: object) -> None: + return super().__init__( + 'workspace window is missing "window_name"', + *args, + **kwargs, + ) + + +class InvalidPluginsValidationError(SchemaValidationError): + def __init__(self, plugins: t.Any, *args: object, **kwargs: object) -> None: + return super().__init__( + '"plugins" only supports list type. ' + + f" Received {type(plugins)}, " + + f"value: {plugins}", + *args, + **kwargs, + ) + + def validate_schema(workspace_dict: t.Any) -> bool: """ Return True if workspace schema is correct. @@ -18,16 +60,16 @@ def validate_schema(workspace_dict: t.Any) -> bool: """ # verify session_name if "session_name" not in workspace_dict: - raise exc.WorkspaceError('workspace requires "session_name"') + raise SessionNameMissingValidationError() if "windows" not in workspace_dict: - raise exc.WorkspaceError('workspace requires list of "windows"') + raise WindowListMissingValidationError() for window in workspace_dict["windows"]: if "window_name" not in window: - raise exc.WorkspaceError('workspace window is missing "window_name"') + raise WindowNameMissingValidationError() if "plugins" in workspace_dict and not isinstance(workspace_dict["plugins"], list): - raise exc.WorkspaceError('"plugins" only supports list type') + raise InvalidPluginsValidationError(plugins=workspace_dict.get("plugins")) return True From f3308972e7ad2808f1afe074b2a8cc11ec25cbff Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Sep 2023 06:49:59 -0500 Subject: [PATCH 07/45] refactor(expandshell): Ignore PTH111 and orient to all strings --- src/tmuxp/workspace/loader.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/tmuxp/workspace/loader.py b/src/tmuxp/workspace/loader.py index dcf1ef090a8..59be53a2825 100644 --- a/src/tmuxp/workspace/loader.py +++ b/src/tmuxp/workspace/loader.py @@ -11,23 +11,22 @@ logger = logging.getLogger(__name__) -def expandshell(_path): - """ - Return expanded path based on user's ``$HOME`` and ``env``. +def expandshell(value: str) -> str: + """Returned wih variables expanded based on user's ``$HOME`` and ``env``. - :py:func:`os.path.expanduser` and :py:func:`os.path.expandvars`. + :py:func:`os.path.expanduser` and :py:fubasednc:`os.path.expandvars`. Parameters ---------- - path : str - path to expand + value : str + value to expand Returns ------- str - path with shell variables expanded + value with shell variables expanded """ - return os.path.expandvars(os.path.expanduser(_path)) + return os.path.expandvars(os.path.expanduser(value)) # NOQA: PTH111 def expand_cmd(p: Dict) -> Dict: From 8ff85c2493bfc3ba5a8a9b42afb30e793a2628b2 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Sep 2023 05:37:02 -0500 Subject: [PATCH 08/45] chore(test_cli): Improve resilient of test in small terminals --- tests/cli/test_cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 3d52bacc924..ea49fb59fe7 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -42,6 +42,10 @@ def test_help( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture, ) -> None: + # In scrunched terminals, prevent width causing differantiation in result.out. + monkeypatch.setenv("COLUMNS", "100") + monkeypatch.setenv("LINES", "100") + with contextlib.suppress(SystemExit): cli.cli(cli_args) From b3fe526cadeb39b9372cdcb45debb95ec60db643 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Sep 2023 05:28:34 -0500 Subject: [PATCH 09/45] chore(mypy): Fix before automated tweaks src/tmuxp/workspace/finders.py:22: error: Incompatible default for argument "extensions" (default has type "None", argument has type "Literal[".yml", ".yaml", ".json"] | list[Literal[".yml", ".yaml", ".json"]]") [assignment] src/tmuxp/workspace/finders.py:22: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True src/tmuxp/workspace/finders.py:22: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase --- src/tmuxp/workspace/finders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tmuxp/workspace/finders.py b/src/tmuxp/workspace/finders.py index 76f2855c701..8ac082ad858 100644 --- a/src/tmuxp/workspace/finders.py +++ b/src/tmuxp/workspace/finders.py @@ -19,7 +19,7 @@ def is_workspace_file( filename: str, - extensions: t.Union["ValidExtensions", t.List["ValidExtensions"]] = None, + extensions: t.Union["ValidExtensions", t.List["ValidExtensions"], None] = None, ) -> bool: """ Return True if file has a valid workspace file type. From 6538efdb987646787c8180e56b2f11073f17337b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 2 Sep 2023 14:49:10 -0500 Subject: [PATCH 10/45] chore(ruff): Manual fixes for tests/workspace/test_finder.py tests/workspace/test_finder.py:56:15: SIM115 Use context handler for opening files tests/workspace/test_finder.py:56:15: PTH123 `open()` should be replaced by `Path.open()` tests/workspace/test_finder.py:156:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:158:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:160:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:162:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:179:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:181:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:183:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:185:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:187:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:201:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:203:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:205:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:207:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:209:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:226:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:228:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:230:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:232:10: B017 `pytest.raises(Exception)` should be considered evil tests/workspace/test_finder.py:234:10: B017 `pytest.raises(Exception)` should be considered evil --- tests/workspace/test_finder.py | 42 +++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/workspace/test_finder.py b/tests/workspace/test_finder.py index 1d64505a4a5..cdb33e7323a 100644 --- a/tests/workspace/test_finder.py +++ b/tests/workspace/test_finder.py @@ -53,8 +53,8 @@ def test_get_configs_cwd( confdir.mkdir() monkeypatch.chdir(confdir) - config1 = open(".tmuxp.json", "w+b") - config1.close() + with pathlib.Path(".tmuxp.json").open("w+b") as config1: + config1.close() configs_found = in_cwd() assert len(configs_found) == 1 @@ -153,13 +153,13 @@ def test_resolve_dot( assert find_workspace_file("myconfig") == str(user_config) assert find_workspace_file("~/.tmuxp/myconfig.yaml") == str(user_config) - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file(".tmuxp.json") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file(".tmuxp.ini") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file("../") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file("mooooooo") monkeypatch.chdir(homedir) @@ -176,15 +176,15 @@ def test_resolve_dot( assert find_workspace_file("myconfig") == str(user_config) assert find_workspace_file("~/.tmuxp/myconfig.yaml") == str(user_config) - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file("") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file(".") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file(".tmuxp.yaml") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file("../") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file("mooooooo") monkeypatch.chdir(configdir) @@ -198,15 +198,15 @@ def test_resolve_dot( assert find_workspace_file("myconfig") == str(user_config) assert find_workspace_file("~/.tmuxp/myconfig.yaml") == str(user_config) - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file("") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file(".") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file(".tmuxp.yaml") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file("../") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file("mooooooo") monkeypatch.chdir(tmp_path) @@ -223,15 +223,15 @@ def test_resolve_dot( assert find_workspace_file("myconfig") == str(user_config) assert find_workspace_file("~/.tmuxp/myconfig.yaml") == str(user_config) - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file("") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file(".") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file(".tmuxp.yaml") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file("../") - with pytest.raises(Exception): + with pytest.raises(FileNotFoundError): find_workspace_file("mooooooo") From 739899729d34848c4f9b19e3a94739536eeaf440 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 2 Sep 2023 18:27:55 -0500 Subject: [PATCH 11/45] chore(ruff): Manual fix for comprehension in tests/workspace/test_config.py --- tests/workspace/test_config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/workspace/test_config.py b/tests/workspace/test_config.py index fb21ef2ca1a..436175d4d79 100644 --- a/tests/workspace/test_config.py +++ b/tests/workspace/test_config.py @@ -1,5 +1,4 @@ """Test for tmuxp configuration import, inlining, expanding and export.""" -import os import pathlib import typing as t @@ -44,14 +43,15 @@ def test_export_json(tmp_path: pathlib.Path, config_fixture: "WorkspaceTestData" # There's no tests for this # def test_find_workspace_file(tmp_path: pathlib.Path): - configs = [] + configs = [str(tmp_path / x) for x in tmp_path.rglob("*.[json][ini][yaml]")] garbage_file = tmp_path / "config.psd" garbage_file.write_text("wat", encoding="utf-8") - for _r, _d, f in os.walk(str(tmp_path)): - for filela in (x for x in f if x.endswith((".json", ".ini", "yaml"))): - configs.append(str(tmp_path / filela)) + # for _r, _d, f in os.walk(str(tmp_path)): + # configs.extend( + # [str(tmp_path / x) for x in f if x.endswith((".json", ".ini", "yaml"))] + # ) files = 0 config_json = tmp_path / "config.json" From 58d19bd8ec4a35d7286d16c0842acc83ae5a8888 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Sep 2023 05:26:01 -0500 Subject: [PATCH 12/45] chore(ruff): Manual fixes test_builder.py --- tests/workspace/test_builder.py | 79 ++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/tests/workspace/test_builder.py b/tests/workspace/test_builder.py index bb8a0f3c806..111ff8b440c 100644 --- a/tests/workspace/test_builder.py +++ b/tests/workspace/test_builder.py @@ -1,4 +1,5 @@ """Test for tmuxp workspacebuilder.""" +import functools import os import pathlib import textwrap @@ -7,7 +8,9 @@ import libtmux import pytest +from libtmux._internal.query_list import ObjectDoesNotExist from libtmux.common import has_gte_version, has_lt_version +from libtmux.exc import LibTmuxException from libtmux.pane import Pane from libtmux.session import Session from libtmux.test import retry_until, temp_session @@ -24,6 +27,11 @@ if t.TYPE_CHECKING: from libtmux.server import Server + from typing_extensions import Protocol + + class AssertCallbackProtocol(Protocol): + def __call__(self, cmd: str, hist: str) -> bool: + ... def test_split_windows(session): @@ -90,9 +98,7 @@ def test_focus_pane_index(session): pane_base_index = 0 if not pane_base_index else int(pane_base_index) # get the pane index for each pane - pane_base_indexes = [] - for pane in session.attached_window.panes: - pane_base_indexes.append(int(pane.index)) + pane_base_indexes = [int(pane.index) for pane in session.attached_window.panes] pane_indexes_should_be = [pane_base_index + x for x in range(0, 3)] assert pane_indexes_should_be == pane_base_indexes @@ -179,7 +185,7 @@ def assertIsMissing(cmd, hist): buffer_name = "test" sent_cmd = None - def f(): + def f(p: Pane, buffer_name: str, assertCase: AssertCallbackProtocol) -> bool: # from v0.7.4 libtmux session.cmd adds target -t self.id by default # show-buffer doesn't accept -t, use global cmd. @@ -195,7 +201,9 @@ def f(): return assertCase(sent_cmd, history_cmd) - assert retry_until(f), f"Unknown sent command: [{sent_cmd}] in {assertCase}" + _f = functools.partial(f, p=p, buffer_name=buffer_name, assertCase=assertCase) + + assert retry_until(_f), f"Unknown sent command: [{sent_cmd}] in {assertCase}" def test_session_options(session): @@ -335,10 +343,12 @@ def test_window_shell(session): if "window_shell" in wconf: assert wconf["window_shell"] == "top" - def f(): + def f(w: Window) -> bool: return w.window_name != "top" - retry_until(f) + _f = functools.partial(f, w=w) + + retry_until(_f) assert w.name != "top" @@ -527,12 +537,16 @@ def test_start_directory(session, tmp_path: pathlib.Path): for path, window in zip(dirs, session.windows): for p in window.panes: - def f(): + def f(path: str, p: Pane) -> bool: pane_path = p.pane_current_path - return path in pane_path or pane_path == path + return ( + pane_path is not None and path in pane_path + ) or pane_path == path + + _f = functools.partial(f, path=path, p=p) # handle case with OS X adding /private/ to /tmp/ paths - assert retry_until(f) + assert retry_until(_f) def test_start_directory_relative(session, tmp_path: pathlib.Path): @@ -564,8 +578,8 @@ def test_start_directory_relative(session, tmp_path: pathlib.Path): workspace = loader.trickle(workspace) - assert os.path.exists(config_dir) - assert os.path.exists(test_dir) + assert config_dir.exists() + assert test_dir.exists() builder = WorkspaceBuilder(session_config=workspace, server=session.server) builder.build(session=session) @@ -576,12 +590,16 @@ def test_start_directory_relative(session, tmp_path: pathlib.Path): for path, window in zip(dirs, session.windows): for p in window.panes: - def f(): - # Handle case where directories resolve to /private/ in OSX + def f(path: str, p: Pane) -> bool: pane_path = p.pane_current_path - return path in pane_path or pane_path == path + return ( + pane_path is not None and path in pane_path + ) or pane_path == path - assert retry_until(f) + _f = functools.partial(f, path=path, p=p) + + # handle case with OS X adding /private/ to /tmp/ paths + assert retry_until(_f) @pytest.mark.skipif( @@ -613,14 +631,14 @@ def test_pane_order(session): """ yaml_workspace = test_utils.read_workspace_file( "workspace/builder/pane_ordering.yaml" - ).format(HOME=os.path.realpath(os.path.expanduser("~"))) + ).format(HOME=str(pathlib.Path().home().resolve())) # test order of `panes` (and pane_index) above against pane_dirs pane_paths = [ "/usr/bin", "/usr", "/etc", - os.path.realpath(os.path.expanduser("~")), + str(pathlib.Path().home().resolve()), ] workspace = ConfigReader._load(format="yaml", content=yaml_workspace) @@ -633,7 +651,7 @@ def test_pane_order(session): assert len(session.windows) == window_count for w, wconf in builder.iter_create_windows(session): - for p in builder.iter_create_panes(w, wconf): + for _ in builder.iter_create_panes(w, wconf): w.select_layout("tiled") # fix glitch with pane size assert len(session.windows) == window_count @@ -651,11 +669,13 @@ def test_pane_order(session): # at 0 since python list. pane_path = pane_paths[p_index - pane_base_index] - def f(): + def f(pane_path: str, p: Pane): p.refresh() return p.pane_current_path == pane_path - assert retry_until(f) + _f = functools.partial(f, pane_path=pane_path, p=p) + + assert retry_until(_f) def test_window_index(session): @@ -1265,12 +1285,14 @@ def test_first_pane_start_directory(session, tmp_path: pathlib.Path): window = session.windows[0] for path, p in zip(dirs, window.panes): - def f(): + def f(path: str, p: Pane) -> bool: pane_path = p.pane_current_path - return path in pane_path or pane_path == path + return (pane_path is not None and path in pane_path) or pane_path == path + + _f = functools.partial(f, path=path, p=p) # handle case with OS X adding /private/ to /tmp/ paths - assert retry_until(f) + assert retry_until(_f) @pytest.mark.skipif( @@ -1381,7 +1403,14 @@ def test_issue_800_default_size_many_windows( builder = WorkspaceBuilder(session_config=workspace, server=server) if raises: - with pytest.raises(Exception): + with pytest.raises( + ( + LibTmuxException, + exc.TmuxpException, + exc.EmptyWorkspaceException, + ObjectDoesNotExist, + ) + ): builder.build() assert builder is not None From 3d05c00c25799621b316711e59f24f48ec97f620 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Sep 2023 05:49:59 -0500 Subject: [PATCH 13/45] chore(ruff): Manual fixes in shell_command_before --- tests/fixtures/workspace/shell_command_before.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/fixtures/workspace/shell_command_before.py b/tests/fixtures/workspace/shell_command_before.py index a952a3db543..69f5c2f2b4e 100644 --- a/tests/fixtures/workspace/shell_command_before.py +++ b/tests/fixtures/workspace/shell_command_before.py @@ -1,5 +1,5 @@ -import os -from typing import Any, Dict +import pathlib +import typing as t config_unexpanded = { # shell_command_before is string in some areas "session_name": "sample workspace", @@ -38,14 +38,14 @@ } -def config_expanded() -> Dict[str, Any]: +def config_expanded() -> t.Dict[str, t.Any]: return { # shell_command_before is string in some areas "session_name": "sample workspace", "start_directory": "/", "windows": [ { "window_name": "editor", - "start_directory": os.path.expanduser("~"), + "start_directory": str(pathlib.Path().home()), "shell_command_before": { "shell_command": [{"cmd": "source .venv/bin/activate"}] }, @@ -90,14 +90,14 @@ def config_expanded() -> Dict[str, Any]: } -def config_after() -> Dict[str, Any]: +def config_after() -> t.Dict[str, t.Any]: return { # shell_command_before is string in some areas "session_name": "sample workspace", "start_directory": "/", "windows": [ { "window_name": "editor", - "start_directory": os.path.expanduser("~"), + "start_directory": str(pathlib.Path().home()), "shell_command_before": { "shell_command": [{"cmd": "source .venv/bin/activate"}] }, From faaff3cf4eadef06323a6d79cef479bfdefe76fa Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Sep 2023 05:57:53 -0500 Subject: [PATCH 14/45] chore(ruff): Manual fixes for expand1, expand2 examples --- tests/fixtures/workspace/expand1.py | 16 +++++----------- tests/fixtures/workspace/expand2.py | 4 ++-- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/fixtures/workspace/expand1.py b/tests/fixtures/workspace/expand1.py index 4cd6c401aad..fa898a06b14 100644 --- a/tests/fixtures/workspace/expand1.py +++ b/tests/fixtures/workspace/expand1.py @@ -1,4 +1,4 @@ -import os +import pathlib before_workspace = { "session_name": "sample workspace", @@ -33,7 +33,7 @@ def after_workspace(): return { "session_name": "sample workspace", - "start_directory": os.path.expanduser("~"), + "start_directory": str(pathlib.Path().home()), "windows": [ { "window_name": "editor", @@ -57,21 +57,15 @@ def after_workspace(): ], }, { - "start_directory": os.path.normpath( - os.path.join(os.path.expanduser("~"), "./") - ), + "start_directory": str(pathlib.Path().home()), "panes": [{"shell_command": [{"cmd": "pwd"}]}], }, { - "start_directory": os.path.normpath( - os.path.join(os.path.expanduser("~"), "./asdf") - ), + "start_directory": str(pathlib.Path().home() / "asdf"), "panes": [{"shell_command": [{"cmd": "pwd"}]}], }, { - "start_directory": os.path.normpath( - os.path.join(os.path.expanduser("~"), "../") - ), + "start_directory": str(pathlib.Path().home().parent.resolve()), "panes": [{"shell_command": [{"cmd": "pwd"}]}], }, {"panes": [{"shell_command": [{"cmd": "top"}]}]}, diff --git a/tests/fixtures/workspace/expand2.py b/tests/fixtures/workspace/expand2.py index 3aa5bd3aa67..21f30403748 100644 --- a/tests/fixtures/workspace/expand2.py +++ b/tests/fixtures/workspace/expand2.py @@ -1,4 +1,4 @@ -import os +import pathlib from .. import utils as test_utils @@ -9,5 +9,5 @@ def unexpanded_yaml(): def expanded_yaml(): return test_utils.read_workspace_file("workspace/expand2-expanded.yaml").format( - HOME=os.path.expanduser("~") + HOME=str(pathlib.Path().home()) ) From 461deb1e51e7db33e9350118ed7d8dac741946f7 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Sep 2023 05:59:24 -0500 Subject: [PATCH 15/45] chore(ruff): Manual fixes fixtures/utils.py --- tests/fixtures/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/fixtures/utils.py b/tests/fixtures/utils.py index d6eb9cbbac4..858227c5b3a 100644 --- a/tests/fixtures/utils.py +++ b/tests/fixtures/utils.py @@ -10,13 +10,16 @@ def get_workspace_file( """Return fixture data, relative to __file__""" if isinstance(_file, str): _file = pathlib.Path(_file) + return FIXTURE_PATH / _file def read_workspace_file(_file: t.Union[pathlib.Path, str]) -> str: """Return fixture data, relative to __file__""" + if isinstance(_file, str): + _file = pathlib.Path(_file) - return open(get_workspace_file(_file)).read() + return get_workspace_file(_file).open().read() def write_config( From 61a8cd5fba3c132f6e9e35db4199a8270d17decc Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Sep 2023 06:00:12 -0500 Subject: [PATCH 16/45] chore(ruff): Manual fix for test_ls --- tests/cli/test_ls.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/cli/test_ls.py b/tests/cli/test_ls.py index fa3c08d0d00..89579d70937 100644 --- a/tests/cli/test_ls.py +++ b/tests/cli/test_ls.py @@ -1,5 +1,4 @@ import contextlib -import os import pathlib import pytest @@ -28,7 +27,7 @@ def test_ls_cli( # - directories should be ignored # - extensions not covered in VALID_WORKSPACE_DIR_FILE_EXTENSIONS ignored_filenames = [".git/", ".gitignore/", "session_4.txt"] - stems = [os.path.splitext(f)[0] for f in filenames if f not in ignored_filenames] + stems = [pathlib.Path(f).stem for f in filenames if f not in ignored_filenames] for filename in filenames: location = tmp_path / f".tmuxp/{filename}" From c5ac6603bc77ba5e49548afabeca7360a26be2a8 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Sep 2023 07:08:03 -0500 Subject: [PATCH 17/45] refactor(ruff): Manual fixes for workspace/loader.py src/tmuxp/workspace/loader.py:51:9: SIM102 Use a single `if` statement instead of nested `if` statements src/tmuxp/workspace/loader.py:102:15: PTH109 `os.getcwd()` should be replaced by `Path.cwd()` src/tmuxp/workspace/loader.py:113:40: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator src/tmuxp/workspace/loader.py:121:44: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator src/tmuxp/workspace/loader.py:129:44: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator src/tmuxp/workspace/loader.py:148:43: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator src/tmuxp/workspace/loader.py:155:17: PTH118 `os.path.join()` should be replaced by `Path` with `/` operator --- src/tmuxp/workspace/loader.py | 37 ++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/tmuxp/workspace/loader.py b/src/tmuxp/workspace/loader.py index 59be53a2825..1668a554547 100644 --- a/src/tmuxp/workspace/loader.py +++ b/src/tmuxp/workspace/loader.py @@ -6,6 +6,7 @@ """ import logging import os +import pathlib from typing import Dict logger = logging.getLogger(__name__) @@ -47,9 +48,12 @@ def expand_cmd(p: Dict) -> Dict: if not cmds or any(a == cmds for a in [None, "blank", "pane"]): cmds = [] - if isinstance(cmds, list) and len(cmds) == 1: - if any(a in cmds for a in [None, "blank", "pane"]): - cmds = [] + if ( + isinstance(cmds, list) + and len(cmds) == 1 + and any(a in cmds for a in [None, "blank", "pane"]) + ): + cmds = [] for cmd_idx, cmd in enumerate(cmds): if isinstance(cmd, str): @@ -97,8 +101,7 @@ def expand(workspace_dict, cwd=None, parent=None): # Note: cli.py will expand workspaces relative to project's workspace directory # for the first cwd argument. - if not cwd: - cwd = os.getcwd() + cwd = pathlib.Path().cwd() if not cwd else pathlib.Path(cwd) if "session_name" in workspace_dict: workspace_dict["session_name"] = expandshell(workspace_dict["session_name"]) @@ -109,7 +112,7 @@ def expand(workspace_dict, cwd=None, parent=None): val = workspace_dict["environment"][key] val = expandshell(val) if any(val.startswith(a) for a in [".", "./"]): - val = os.path.normpath(os.path.join(cwd, val)) + val = str(cwd / val) workspace_dict["environment"][key] = val if "global_options" in workspace_dict: for key in workspace_dict["global_options"]: @@ -117,7 +120,7 @@ def expand(workspace_dict, cwd=None, parent=None): if isinstance(val, str): val = expandshell(val) if any(val.startswith(a) for a in [".", "./"]): - val = os.path.normpath(os.path.join(cwd, val)) + val = str(cwd / val) workspace_dict["global_options"][key] = val if "options" in workspace_dict: for key in workspace_dict["options"]: @@ -125,7 +128,7 @@ def expand(workspace_dict, cwd=None, parent=None): if isinstance(val, str): val = expandshell(val) if any(val.startswith(a) for a in [".", "./"]): - val = os.path.normpath(os.path.join(cwd, val)) + val = str(cwd / val) workspace_dict["options"][key] = val # Any workspace section, session, window, pane that can contain the @@ -143,16 +146,16 @@ def expand(workspace_dict, cwd=None, parent=None): # This is for the case where you may be loading a workspace from # outside your shell current directory. if parent: - cwd = parent["start_directory"] - start_path = os.path.normpath(os.path.join(cwd, start_path)) + cwd = pathlib.Path(parent["start_directory"]) + + start_path = str((cwd / start_path).resolve(strict=False)) + workspace_dict["start_directory"] = start_path if "before_script" in workspace_dict: workspace_dict["before_script"] = expandshell(workspace_dict["before_script"]) if any(workspace_dict["before_script"].startswith(a) for a in [".", "./"]): - workspace_dict["before_script"] = os.path.normpath( - os.path.join(cwd, workspace_dict["before_script"]) - ) + workspace_dict["before_script"] = str(cwd / workspace_dict["before_script"]) if "shell_command" in workspace_dict and isinstance( workspace_dict["shell_command"], str @@ -212,16 +215,18 @@ def trickle(workspace_dict): for window_dict in workspace_dict["windows"]: # Prepend start_directory to relative window commands if session_start_directory: + session_start_directory = session_start_directory if "start_directory" not in window_dict: window_dict["start_directory"] = session_start_directory else: if not any( window_dict["start_directory"].startswith(a) for a in ["~", "/"] ): - window_start_path = os.path.join( - session_start_directory, window_dict["start_directory"] + window_start_path = ( + pathlib.Path(session_start_directory) + / window_dict["start_directory"] ) - window_dict["start_directory"] = window_start_path + window_dict["start_directory"] = str(window_start_path) # We only need to trickle to the window, workspace builder checks wconf if suppress_history is not None and "suppress_history" not in window_dict: From e85578089591125a1049d9191a678ff7cebb6461 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 3 Sep 2023 08:09:50 -0500 Subject: [PATCH 18/45] chore(ruff): Manual typings for workspace/freezer src/tmuxp/workspace/freezer.py:81:20: B023 Function definition does not bind loop variable `window` src/tmuxp/workspace/freezer.py:99:24: B023 Function definition does not bind loop variable `current_cmd` src/tmuxp/workspace/freezer.py:100:21: B023 Function definition does not bind loop variable `current_cmd` src/tmuxp/workspace/freezer.py:102:25: B023 Function definition does not bind loop variable `current_cmd` --- src/tmuxp/workspace/freezer.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/tmuxp/workspace/freezer.py b/src/tmuxp/workspace/freezer.py index ab29b36caaf..8f38e7be4bd 100644 --- a/src/tmuxp/workspace/freezer.py +++ b/src/tmuxp/workspace/freezer.py @@ -3,6 +3,9 @@ from libtmux.pane import Pane from libtmux.session import Session +if t.TYPE_CHECKING: + from libtmux.window import Window + def inline(workspace_dict): """Return workspace with inlined shorthands. Opposite of :meth:`loader.expand`. @@ -77,10 +80,10 @@ def freeze(session: Session) -> t.Dict[str, t.Any]: # If all panes have same path, set 'start_directory' instead # of using 'cd' shell commands. - def pane_has_same_path(pane: Pane) -> bool: + def pane_has_same_path(window: "Window", pane: Pane) -> bool: return window.panes[0].pane_current_path == pane.pane_current_path - if all(pane_has_same_path(pane=pane) for pane in window.panes): + if all(pane_has_same_path(window=window, pane=pane) for pane in window.panes): window_config["start_directory"] = window.panes[0].pane_current_path for pane in window.panes: @@ -95,7 +98,7 @@ def pane_has_same_path(pane: Pane) -> bool: current_cmd = pane.pane_current_command - def filter_interpretters_and_shells() -> bool: + def filter_interpretters_and_shells(current_cmd: t.Optional[str]) -> bool: return current_cmd is not None and ( current_cmd.startswith("-") or any( @@ -103,7 +106,7 @@ def filter_interpretters_and_shells() -> bool: ) ) - if filter_interpretters_and_shells(): + if filter_interpretters_and_shells(current_cmd=current_cmd): current_cmd = None if current_cmd: From d4a5aa08352791e9ea2d140b8f66cadf67fd05c2 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:08:10 -0500 Subject: [PATCH 19/45] ci(ruff): Ignore PTH in workspace/finders.py This relies a lot on supporting tmuxp files and directory strings, e.g. "my_session" equating to ~/config/.tmuxp/my_session.{yaml,json} and tmuxp load . which equates to cwd .tmuxp.{yaml.json}. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 8b95af45cc3..fc53280983d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,6 +180,7 @@ combine-as-imports = true [tool.ruff.per-file-ignores] "*/__init__.py" = ["F401"] +"src/tmuxp/workspace/finders.py" = ["PTH"] [build-system] requires = ["poetry_core>=1.0.0"] From a851230fbfa28f808f4d8ada424120e3a47bb65b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:10:19 -0500 Subject: [PATCH 20/45] chore(ruff): Manual fixes for workspace/finders.py --- src/tmuxp/workspace/finders.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/tmuxp/workspace/finders.py b/src/tmuxp/workspace/finders.py index 8ac082ad858..16b805c498f 100644 --- a/src/tmuxp/workspace/finders.py +++ b/src/tmuxp/workspace/finders.py @@ -42,7 +42,7 @@ def is_workspace_file( def in_dir( - workspace_dir: t.Union[pathlib.Path, str] = os.path.expanduser("~/.tmuxp"), + workspace_dir: t.Union[pathlib.Path, str, None] = None, extensions: t.Optional[t.List["ValidExtensions"]] = None, ) -> t.List[str]: """ @@ -59,13 +59,17 @@ def in_dir( ------- list """ + if workspace_dir is None: + workspace_dir = os.path.expanduser("~/.tmuxp") + if extensions is None: extensions = [".yml", ".yaml", ".json"] - workspace_files = [] - for filename in os.listdir(workspace_dir): - if is_workspace_file(filename, extensions) and not filename.startswith("."): - workspace_files.append(filename) + workspace_files = [ + filename + for filename in os.listdir(workspace_dir) + if is_workspace_file(filename, extensions) and not filename.startswith(".") + ] return workspace_files @@ -86,11 +90,11 @@ def in_cwd() -> t.List[str]: >>> sorted(in_cwd()) ['.tmuxp.json', '.tmuxp.yaml'] """ - workspace_files = [] - - for filename in os.listdir(os.getcwd()): - if filename.startswith(".tmuxp") and is_workspace_file(filename): - workspace_files.append(filename) + workspace_files = [ + filename + for filename in os.listdir(os.getcwd()) + if filename.startswith(".tmuxp") and is_workspace_file(filename) + ] return workspace_files From a6e925a91800701719d843020d15229b03b2338f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:14:22 -0500 Subject: [PATCH 21/45] refactor(builder): Move error message to EmptyWorkspaceException --- src/tmuxp/exc.py | 3 +++ src/tmuxp/workspace/builder.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tmuxp/exc.py b/src/tmuxp/exc.py index beb9bbc5431..42b1eb968ac 100644 --- a/src/tmuxp/exc.py +++ b/src/tmuxp/exc.py @@ -23,6 +23,9 @@ class EmptyWorkspaceException(WorkspaceError): """Workspace file is empty.""" + def __init__(self, *args: object, **kwargs: object) -> None: + return super().__init__("Session configuration is empty.", *args, **kwargs) + class TmuxpPluginException(TmuxpException): diff --git a/src/tmuxp/workspace/builder.py b/src/tmuxp/workspace/builder.py index 61b5c937bdc..0fc16b3943a 100644 --- a/src/tmuxp/workspace/builder.py +++ b/src/tmuxp/workspace/builder.py @@ -165,7 +165,7 @@ def __init__( if plugins is None: plugins = [] if not session_config: - raise exc.EmptyWorkspaceException("Session configuration is empty.") + raise exc.EmptyWorkspaceException() # validation.validate_schema(session_config) From 3ad5cf1b625173e7323ee58a09fd32bbc8228324 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:16:06 -0500 Subject: [PATCH 22/45] feat: Add SessionMissingWorkspaceException --- src/tmuxp/exc.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/tmuxp/exc.py b/src/tmuxp/exc.py index 42b1eb968ac..764bf2ccb3c 100644 --- a/src/tmuxp/exc.py +++ b/src/tmuxp/exc.py @@ -6,6 +6,8 @@ """ import typing as t +from libtmux._internal.query_list import ObjectDoesNotExist + from ._compat import implements_to_string @@ -27,6 +29,16 @@ def __init__(self, *args: object, **kwargs: object) -> None: return super().__init__("Session configuration is empty.", *args, **kwargs) +class SessionMissingWorkspaceException(WorkspaceError, ObjectDoesNotExist): + def __init__(self, *args: object, **kwargs: object) -> None: + return super().__init__( + "No session object exists for WorkspaceBuilder. " + "Tip: Add session_name in constructor or run WorkspaceBuilder.build()", + *args, + **kwargs, + ) + + class TmuxpPluginException(TmuxpException): """Base Exception for Tmuxp Errors.""" From e570ca614702c9db4385a0be196aebcfa8a60f61 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:17:08 -0500 Subject: [PATCH 23/45] chore(WorkspaceBuilder): Move to dedicated exception for missing session --- src/tmuxp/workspace/builder.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/tmuxp/workspace/builder.py b/src/tmuxp/workspace/builder.py index 0fc16b3943a..3665fe0fbcb 100644 --- a/src/tmuxp/workspace/builder.py +++ b/src/tmuxp/workspace/builder.py @@ -190,10 +190,7 @@ def __init__( @property def session(self): if self._session is None: - raise ObjectDoesNotExist( - "No session object exists for WorkspaceBuilder. " - "Tip: Add session_name in constructor or run WorkspaceBuilder.build()" - ) + raise exc.SessionMissingWorkspaceException() return self._session def session_exists(self, session_name: str) -> bool: From 64004d900ae98f9a7699d1ae275932503c7f587e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:18:13 -0500 Subject: [PATCH 24/45] refactor(exc): Add ActiveSessionMissingWorkspaceException --- src/tmuxp/exc.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tmuxp/exc.py b/src/tmuxp/exc.py index 764bf2ccb3c..e3ad66046d5 100644 --- a/src/tmuxp/exc.py +++ b/src/tmuxp/exc.py @@ -39,6 +39,11 @@ def __init__(self, *args: object, **kwargs: object) -> None: ) +class ActiveSessionMissingWorkspaceException(WorkspaceError): + def __init__(self, *args: object, **kwargs: object) -> None: + return super().__init__("No session active.", *args, **kwargs) + + class TmuxpPluginException(TmuxpException): """Base Exception for Tmuxp Errors.""" From 5f23e3b76f701d6c89a9c305878892e862126e8e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:18:37 -0500 Subject: [PATCH 25/45] chore(WorkspaceBuilder): Add ActiveSessionMissingWorkspaceException --- src/tmuxp/workspace/builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tmuxp/workspace/builder.py b/src/tmuxp/workspace/builder.py index 3665fe0fbcb..57e2e3cdd0b 100644 --- a/src/tmuxp/workspace/builder.py +++ b/src/tmuxp/workspace/builder.py @@ -554,7 +554,7 @@ def find_current_attached_session(self) -> Session: current_active_pane = get_current_pane(self.server) if current_active_pane is None: - raise exc.TmuxpException("No session active.") + raise exc.ActiveSessionMissingWorkspaceException() return next( ( From f66d6527d5d49f19be23de1470a75a63b0fa273f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:22:44 -0500 Subject: [PATCH 26/45] chore(ruff): Manual fixes for workspace/builder.py src/tmuxp/workspace/builder.py:258:9: B018 Found useless expression. Either assign it to a variable or remove it. src/tmuxp/workspace/builder.py:456:45: B023 Function definition does not bind loop variable `pane_config` src/tmuxp/workspace/builder.py:457:32: B023 Function definition does not bind loop variable `pane_config` src/tmuxp/workspace/builder.py:464:35: B023 Function definition does not bind loop variable `pane_config` src/tmuxp/workspace/builder.py:465:32: B023 Function definition does not bind loop variable `pane_config` --- src/tmuxp/workspace/builder.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/tmuxp/workspace/builder.py b/src/tmuxp/workspace/builder.py index 57e2e3cdd0b..b73a250fecd 100644 --- a/src/tmuxp/workspace/builder.py +++ b/src/tmuxp/workspace/builder.py @@ -255,7 +255,7 @@ def build(self, session: t.Optional[Session] = None, append: bool = False) -> No assert session.server is not None self.server: "Server" = session.server - self.server.sessions + assert self.server.sessions is not None assert self.server.has_session(session.name) assert session.id @@ -452,7 +452,9 @@ def iter_create_panes( pane = window.attached_pane else: - def get_pane_start_directory(): + def get_pane_start_directory( + pane_config: t.Dict[str, str], window_config: t.Dict[str, str] + ) -> t.Optional[str]: if "start_directory" in pane_config: return pane_config["start_directory"] elif "start_directory" in window_config: @@ -460,7 +462,9 @@ def get_pane_start_directory(): else: return None - def get_pane_shell(): + def get_pane_shell( + pane_config: t.Dict[str, str], window_config: t.Dict[str, str] + ) -> t.Optional[str]: if "shell" in pane_config: return pane_config["shell"] elif "window_shell" in window_config: @@ -486,8 +490,14 @@ def get_pane_shell(): pane = window.split_window( attach=True, - start_directory=get_pane_start_directory(), - shell=get_pane_shell(), + start_directory=get_pane_start_directory( + pane_config=pane_config, + window_config=window_config, + ), + shell=get_pane_shell( + pane_config=pane_config, + window_config=window_config, + ), target=pane.id, environment=environment, ) From 9cda65aef3c08b0b54890b656e68124bb9e13b31 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:28:24 -0500 Subject: [PATCH 27/45] chore(ruff): Manual fixes for tmuxp/util.py src/tmuxp/util.py:53:34: PTH100 `os.path.abspath()` should be replaced by `Path.resolve()` src/tmuxp/util.py:56:9: TRY300 Consider moving this statement to an `else` block src/tmuxp/util.py:59:13: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling src/tmuxp/util.py:59:13: TRY200 Use `raise from` to specify exception cause src/tmuxp/util.py:59:52: PTH100 `os.path.abspath()` should be replaced by `Path.resolve()` src/tmuxp/util.py:69:5: SIM102 Use a single `if` statement instead of nested `if` statements src/tmuxp/util.py:70:9: SIM102 Use a single `if` statement instead of nested `if` statements src/tmuxp/util.py:70:12: PTH110 `os.path.exists()` should be replaced by `Path.exists()` src/tmuxp/util.py:70:27: PTH111 `os.path.expanduser()` should be replaced by `Path.expanduser()` --- src/tmuxp/util.py | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/tmuxp/util.py b/src/tmuxp/util.py index 3d313a50c4e..7d7e616be82 100644 --- a/src/tmuxp/util.py +++ b/src/tmuxp/util.py @@ -50,13 +50,18 @@ def run_before_script( stderr_str = "\n".join(list(filter(None, stderr_strlist))) # filter empty raise exc.BeforeLoadScriptError( - proc.returncode, os.path.abspath(script_file), stderr_str + proc.returncode, + os.path.abspath(script_file), # NOQA: PTH100 + stderr_str, ) - - return proc.returncode + else: + return proc.returncode except OSError as e: if e.errno == 2: - raise exc.BeforeLoadScriptNotExists(e, os.path.abspath(script_file)) + raise exc.BeforeLoadScriptNotExists( + e, + os.path.abspath(script_file), # NOQA: PTH100 + ) from e else: raise @@ -66,21 +71,23 @@ def oh_my_zsh_auto_title() -> None: See: https://github.com/robbyrussell/oh-my-zsh/pull/257 """ - if "SHELL" in os.environ and "zsh" in os.environ.get("SHELL", ""): - if os.path.exists(os.path.expanduser("~/.oh-my-zsh")): - # oh-my-zsh exists - if ( - "DISABLE_AUTO_TITLE" not in os.environ - or os.environ.get("DISABLE_AUTO_TITLE") == "false" - ): - print( - "Please set:\n\n" - "\texport DISABLE_AUTO_TITLE='true'\n\n" - "in ~/.zshrc or where your zsh profile is stored.\n" - 'Remember the "export" at the beginning!\n\n' - "Then create a new shell or type:\n\n" - "\t$ source ~/.zshrc" - ) + if ( + "SHELL" in os.environ + and "zsh" in os.environ.get("SHELL", "") + and os.path.exists(os.path.expanduser("~/.oh-my-zsh")) # NOQA PTH110, PTH111 + and ( + "DISABLE_AUTO_TITLE" not in os.environ + or os.environ.get("DISABLE_AUTO_TITLE") == "false" + ) + ): + print( + "Please set:\n\n" + "\texport DISABLE_AUTO_TITLE='true'\n\n" + "in ~/.zshrc or where your zsh profile is stored.\n" + 'Remember the "export" at the beginning!\n\n' + "Then create a new shell or type:\n\n" + "\t$ source ~/.zshrc" + ) def get_current_pane(server: "Server") -> t.Optional["Pane"]: From 1232510113bf1243b03537d7246ff1451a93a74b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:35:20 -0500 Subject: [PATCH 28/45] feat(exc): {Pane,Window,Session}NotFound --- src/tmuxp/exc.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/tmuxp/exc.py b/src/tmuxp/exc.py index e3ad66046d5..5fb0d9f722f 100644 --- a/src/tmuxp/exc.py +++ b/src/tmuxp/exc.py @@ -21,6 +21,36 @@ class WorkspaceError(TmuxpException): """Error parsing tmuxp workspace data.""" +class SessionNotFound(TmuxpException): + def __init__( + self, session_target: t.Optional[str] = None, *args: object, **kwargs: object + ) -> None: + msg = "Session not found" + if session_target is not None: + msg += f": {session_target}" + return super().__init__(msg, *args, **kwargs) + + +class WindowNotFound(TmuxpException): + def __init__( + self, window_target: t.Optional[str] = None, *args: object, **kwargs: object + ) -> None: + msg = "Window not found" + if window_target is not None: + msg += f": {window_target}" + return super().__init__(msg, *args, **kwargs) + + +class PaneNotFound(TmuxpException): + def __init__( + self, pane_target: t.Optional[str] = None, *args: object, **kwargs: object + ) -> None: + msg = "Pane not found" + if pane_target is not None: + msg += f": {pane_target}" + return super().__init__(msg, *args, **kwargs) + + class EmptyWorkspaceException(WorkspaceError): """Workspace file is empty.""" From ce9d20b89a73d19bd34d907a51cf92ebefa2e46a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:36:42 -0500 Subject: [PATCH 29/45] refactor(util): Consoidate get_{pane,window,session} --- src/tmuxp/util.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/tmuxp/util.py b/src/tmuxp/util.py index 7d7e616be82..a0adda949be 100644 --- a/src/tmuxp/util.py +++ b/src/tmuxp/util.py @@ -116,15 +116,13 @@ def get_session( session = server.sessions.get(session_id=current_pane.session_id) else: session = server.sessions[0] - except Exception: - session = None - if session is None: + except Exception as e: if session_name: - raise exc.TmuxpException("Session not found: %s" % session_name) - else: - raise exc.TmuxpException("Session not found") + raise exc.TmuxpException("Session not found: %s" % session_name) from e + raise exc.TmuxpException("Session not found") from e + assert session is not None return session @@ -140,17 +138,14 @@ def get_window( window = session.windows.get(window_id=current_pane.window_id) else: window = session.windows[0] - except Exception: - window = None - - if window is None: + except Exception as e: if window_name: - raise exc.TmuxpException("Window not found: %s" % window_name) + raise exc.TmuxpException("Window not found: %s" % window_name) from e if current_pane: - raise exc.TmuxpException("Window not found: %s" % current_pane) - else: - raise exc.TmuxpException("Window not found") + raise exc.TmuxpException("Window not found: %s" % current_pane) from e + raise exc.TmuxpException("Window not found") from e + assert window is not None return window From 27b351801383db8cd1e6731d2abd621dfbd10318 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:42:59 -0500 Subject: [PATCH 30/45] refactor(util): Use new NotFound exceptions, organize logic --- src/tmuxp/util.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/tmuxp/util.py b/src/tmuxp/util.py index a0adda949be..8a306fbedf7 100644 --- a/src/tmuxp/util.py +++ b/src/tmuxp/util.py @@ -119,8 +119,8 @@ def get_session( except Exception as e: if session_name: - raise exc.TmuxpException("Session not found: %s" % session_name) from e - raise exc.TmuxpException("Session not found") from e + raise exc.SessionNotFound(session_name) from e + raise exc.SessionNotFound() from e assert session is not None return session @@ -140,10 +140,10 @@ def get_window( window = session.windows[0] except Exception as e: if window_name: - raise exc.TmuxpException("Window not found: %s" % window_name) from e - if current_pane: - raise exc.TmuxpException("Window not found: %s" % current_pane) from e - raise exc.TmuxpException("Window not found") from e + raise exc.WindowNotFound(window_target=window_name) from e + elif current_pane: + raise exc.WindowNotFound(window_target=str(current_pane)) from e + raise exc.WindowNotFound() from e assert window is not None return window @@ -161,8 +161,7 @@ def get_pane(window: "Window", current_pane: t.Optional["Pane"] = None) -> "Pane if pane is None: if current_pane: - raise exc.TmuxpException("Pane not found: %s" % current_pane) - else: - raise exc.TmuxpException("Pane not found") + raise exc.PaneNotFound(str(current_pane)) + raise exc.PaneNotFound() return pane From b6621bd846ecc548e03ed040756a282523494b4f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:56:49 -0500 Subject: [PATCH 31/45] chore(ruff): Manual fixes for shell --- src/tmuxp/shell.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/tmuxp/shell.py b/src/tmuxp/shell.py index 378e8b9755d..0f73721d6c3 100644 --- a/src/tmuxp/shell.py +++ b/src/tmuxp/shell.py @@ -6,6 +6,7 @@ """ import logging import os +import pathlib import typing as t logger = logging.getLogger(__name__) @@ -106,7 +107,7 @@ def launch_ipython(): ipython_arguments = extra_args or get_ipython_arguments() start_ipython(argv=ipython_arguments, user_ns=imported_objects) - return launch_ipython + return launch_ipython # NOQA: TRY300 except ImportError: # IPython < 0.11 # Explicitly pass an empty list as arguments, because otherwise @@ -130,7 +131,7 @@ def get_ptpython(options, vi_mode=False): def launch_ptpython(): imported_objects = get_launch_args(**options) - history_filename = os.path.expanduser("~/.ptpython_history") + history_filename = str(pathlib.Path("~/.ptpython_history").expanduser()) embed( globals=imported_objects, history_filename=history_filename, @@ -156,7 +157,7 @@ def get_ptipython(options, vi_mode=False): def launch_ptipython(): imported_objects = get_launch_args(**options) - history_filename = os.path.expanduser("~/.ptpython_history") + history_filename = str(pathlib.Path("~/.ptpython_history").expanduser()) embed( user_ns=imported_objects, history_filename=history_filename, @@ -169,13 +170,17 @@ def launch_ptipython(): def get_launch_args(**kwargs): import libtmux + from libtmux.pane import Pane + from libtmux.server import Server + from libtmux.session import Session + from libtmux.window import Window return { "libtmux": libtmux, - "Server": libtmux.Server, - "Session": libtmux.Session, - "Window": libtmux.Window, - "Pane": libtmux.Pane, + "Server": Server, + "Session": Session, + "Window": Window, + "Pane": Pane, "server": kwargs.get("server"), "session": kwargs.get("session"), "window": kwargs.get("window"), @@ -208,15 +213,16 @@ def get_code(use_pythonrc, imported_objects): # We want to honor both $PYTHONSTARTUP and .pythonrc.py, so follow system # conventions and get $PYTHONSTARTUP first then .pythonrc.py. if use_pythonrc: + PYTHONSTARTUP = os.environ.get("PYTHONSTARTUP") for pythonrc in { - os.environ.get("PYTHONSTARTUP"), - os.path.expanduser("~/.pythonrc.py"), + *([pathlib.Path(PYTHONSTARTUP)] if PYTHONSTARTUP is not None else []), + pathlib.Path("~/.pythonrc.py").expanduser(), }: if not pythonrc: continue - if not os.path.isfile(pythonrc): + if not pythonrc.is_file(): continue - with open(pythonrc) as handle: + with pythonrc.open() as handle: pythonrc_code = handle.read() # Match the behavior of the cpython shell where an error in # PYTHONSTARTUP prints an exception and continues. From d7f6a7573fd484dc8387a89dc4151733346c47b5 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:58:01 -0500 Subject: [PATCH 32/45] chore(ruff): Manual fixes for plugin src/tmuxp/plugin.py:143:17: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling src/tmuxp/plugin.py:143:17: TRY200 Use `raise from` to specify exception cause --- src/tmuxp/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tmuxp/plugin.py b/src/tmuxp/plugin.py index 8417d28f6c1..7d56316630c 100644 --- a/src/tmuxp/plugin.py +++ b/src/tmuxp/plugin.py @@ -139,14 +139,14 @@ def _version_check(self) -> None: assert isinstance(constraints, dict) try: assert self._pass_version_check(**constraints) - except AssertionError: + except AssertionError as e: raise TmuxpPluginException( "Incompatible {dep} version: {version}\n{plugin_name} " "requirements:\nmin: {vmin} | max: {vmax} | " "incompatible: {incompatible}\n".format( dep=dep, plugin_name=self.plugin_name, **constraints ) - ) + ) from e def _pass_version_check( self, From 256774734ff7633ba37d4498374f27a6816def4a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 05:58:58 -0500 Subject: [PATCH 33/45] chore(ruff): Manual fixes for config_reader src/tmuxp/config_reader.py:102:19: SIM115 Use context handler for opening files src/tmuxp/config_reader.py:102:19: PTH123 `open()` should be replaced by `Path.open()` --- src/tmuxp/config_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tmuxp/config_reader.py b/src/tmuxp/config_reader.py index 6f3683c6dbb..b3562da7d7a 100644 --- a/src/tmuxp/config_reader.py +++ b/src/tmuxp/config_reader.py @@ -99,7 +99,7 @@ def _from_file(cls, path: pathlib.Path) -> t.Dict[str, t.Any]: {'session_name': 'my session'} """ assert isinstance(path, pathlib.Path) - content = open(path).read() + content = path.open().read() if path.suffix in [".yaml", ".yml"]: format: "FormatLiteral" = "yaml" From 40d87b08284e3dbff1117ace3c9a3bf7a55fe2b6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 06:00:54 -0500 Subject: [PATCH 34/45] feat(cli[utils]): Add UnknownStyleColor --- src/tmuxp/cli/utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/tmuxp/cli/utils.py b/src/tmuxp/cli/utils.py index b38ca689490..d4e06f1a75f 100644 --- a/src/tmuxp/cli/utils.py +++ b/src/tmuxp/cli/utils.py @@ -183,6 +183,11 @@ def _interpret_color( return str(_ansi_colors[color] + offset) +class UnknownStyleColor(Exception): + def __init__(self, color: str, *args: object, **kwargs: object) -> None: + return super().__init__(f"Unknown color {color!r}", *args, **kwargs) + + def style( text: t.Any, fg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None, From e0dab28655ad8a07e3ac2a37570c711898398771 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 06:02:41 -0500 Subject: [PATCH 35/45] feat(cli[utils]): Extract CLIColour to type --- src/tmuxp/cli/utils.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/tmuxp/cli/utils.py b/src/tmuxp/cli/utils.py index d4e06f1a75f..a98caf33924 100644 --- a/src/tmuxp/cli/utils.py +++ b/src/tmuxp/cli/utils.py @@ -4,6 +4,12 @@ from .. import log +if t.TYPE_CHECKING: + from typing_extensions import TypeAlias + + CLIColour: TypeAlias = t.Union[int, t.Tuple[int, int, int], str] + + logger = logging.getLogger(__name__) @@ -184,14 +190,14 @@ def _interpret_color( class UnknownStyleColor(Exception): - def __init__(self, color: str, *args: object, **kwargs: object) -> None: + def __init__(self, color: "CLIColour", *args: object, **kwargs: object) -> None: return super().__init__(f"Unknown color {color!r}", *args, **kwargs) def style( text: t.Any, - fg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None, - bg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None, + fg: t.Optional["CLIColour"] = None, + bg: t.Optional["CLIColour"] = None, bold: t.Optional[bool] = None, dim: t.Optional[bool] = None, underline: t.Optional[bool] = None, From b70188bdb538db8b28e5a4a91aa9d7bf308244e1 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 06:04:34 -0500 Subject: [PATCH 36/45] ruff(chore): Manual fixes for cli/utils.py via new exceptions src/tmuxp/cli/utils.py:218:19: TRY003 Avoid specifying long messages outside the exception class src/tmuxp/cli/utils.py:224:19: TRY003 Avoid specifying long messages outside the exception class --- src/tmuxp/cli/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tmuxp/cli/utils.py b/src/tmuxp/cli/utils.py index a98caf33924..91f2ef165c5 100644 --- a/src/tmuxp/cli/utils.py +++ b/src/tmuxp/cli/utils.py @@ -218,13 +218,13 @@ def style( try: bits.append(f"\033[{_interpret_color(fg)}m") except KeyError: - raise TypeError(f"Unknown color {fg!r}") from None + raise UnknownStyleColor(color=fg) from None if bg: try: bits.append(f"\033[{_interpret_color(bg, 10)}m") except KeyError: - raise TypeError(f"Unknown color {bg!r}") from None + raise UnknownStyleColor(color=bg) from None if bold is not None: bits.append(f"\033[{1 if bold else 22}m") From 37bf0b1324183d63f580febdaad841367835fbe2 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 06:06:18 -0500 Subject: [PATCH 37/45] chore(ruff): Fix typings docs/conf.py:204:9: PERF203 `try`-`except` within a loop incurs performance overhead --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index fb81f6f9c4e..fea1c2e785f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -200,7 +200,7 @@ def linkcode_resolve(domain, info): for part in fullname.split("."): try: obj = getattr(obj, part) - except Exception: + except Exception: # NOQA: PERF203 return None # strip decorators, which would resolve to the source of the decorator From 34174928324357ca19ada1618ddecc4d2fad9a08 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 06:09:16 -0500 Subject: [PATCH 38/45] ci(ruff): Ignore cli/*.py, aafig.py --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index fc53280983d..e90bba676ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -181,6 +181,8 @@ combine-as-imports = true [tool.ruff.per-file-ignores] "*/__init__.py" = ["F401"] "src/tmuxp/workspace/finders.py" = ["PTH"] +"src/tmuxp/cli/*.py" = ["PTH"] +"docs/_ext/aafig.py" = ["PTH"] [build-system] requires = ["poetry_core>=1.0.0"] From abc4344318bdbf1e3cab406ca19e112758fb44a6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 06:11:04 -0500 Subject: [PATCH 39/45] chore(ruff): Manual fix for conftest.py conftest.py:72:37: PTH100 `os.path.abspath()` should be replaced by `Path.resolve()` --- conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index aa132bb30b3..024ccbf9d96 100644 --- a/conftest.py +++ b/conftest.py @@ -69,7 +69,7 @@ def monkeypatch_plugin_test_packages(monkeypatch: pytest.MonkeyPatch) -> None: "tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/", ] for path in paths: - monkeypatch.syspath_prepend(os.path.abspath(os.path.relpath(path))) + monkeypatch.syspath_prepend(str(pathlib.Path(path).resolve())) @pytest.fixture(scope="function") From 58b6b47e7e4cdf3b40caa0f906fda42677e0c20a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 06:15:11 -0500 Subject: [PATCH 40/45] chore(ruff): Manual fixes for cli/load.py rc/tmuxp/cli/load.py:122:13: PERF203 `try`-`except` within a loop incurs performance overhead src/tmuxp/cli/load.py:124:89: E501 Line too long (93 > 88 characters) --- src/tmuxp/cli/load.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/tmuxp/cli/load.py b/src/tmuxp/cli/load.py index 8ad5c475427..de21db1aa6a 100644 --- a/src/tmuxp/cli/load.py +++ b/src/tmuxp/cli/load.py @@ -115,12 +115,25 @@ def load_plugins(session_config: t.Dict[str, t.Any]) -> t.List[t.Any]: module_name = plugin.split(".") module_name = ".".join(module_name[:-1]) plugin_name = plugin.split(".")[-1] + except Exception as error: + tmuxp_echo( + style("[Plugin Error] ", fg="red") + + f"Couldn't load {plugin}\n" + + style(f"{error}", fg="yellow") + ) + sys.exit(1) + + try: plugin = getattr(importlib.import_module(module_name), plugin_name) plugins.append(plugin()) except exc.TmuxpPluginException as error: if not prompt_yes_no( "{}Skip loading {}?".format( - style(str(error), fg="yellow"), plugin_name + style( + str(error), + fg="yellow", + ), + plugin_name, ), default=True, ): From 4b1425ad6e0957e4800774d972159063fbe1340e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 06:17:15 -0500 Subject: [PATCH 41/45] chore(ruff): Manual fixes for cli/freeze.py src/tmuxp/cli/import_config.py:156:15: SIM115 Use context handler for opening files --- src/tmuxp/cli/import_config.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tmuxp/cli/import_config.py b/src/tmuxp/cli/import_config.py index b2296caded5..301f0dd8cfb 100644 --- a/src/tmuxp/cli/import_config.py +++ b/src/tmuxp/cli/import_config.py @@ -153,9 +153,8 @@ def import_config( if prompt_yes_no("Save to %s?" % dest_path): dest = dest_path - buf = open(dest, "w") - buf.write(new_config) - buf.close() + with open(dest, "w") as buf: + buf.write(new_config) tmuxp_echo("Saved to %s." % dest) else: From 192741071be3dabfc2efcc265266880b116a832c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 06:18:24 -0500 Subject: [PATCH 42/45] chore(ruff): Manual fixes for cli/freeze.py src/tmuxp/cli/freeze.py:109:13: TRY301 Abstract `raise` to an inner function src/tmuxp/cli/freeze.py:109:19: TRY003 Avoid specifying long messages outside the exception class src/tmuxp/cli/freeze.py:198:15: SIM115 Use context handler for opening files --- src/tmuxp/cli/freeze.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/tmuxp/cli/freeze.py b/src/tmuxp/cli/freeze.py index bd0cf1dac4d..0f22257fc18 100644 --- a/src/tmuxp/cli/freeze.py +++ b/src/tmuxp/cli/freeze.py @@ -10,7 +10,7 @@ from tmuxp.exc import TmuxpException from tmuxp.workspace.finders import get_workspace_dir -from .. import util +from .. import exc, util from ..workspace import freezer from .utils import prompt, prompt_choices, prompt_yes_no @@ -106,7 +106,7 @@ def command_freeze( session = util.get_session(server) if not session: - raise TmuxpException("Session not found.") + raise exc.SessionNotFound() except TmuxpException as e: print(e) return @@ -195,9 +195,8 @@ def extract_workspace_format( destdir = os.path.dirname(dest) if not os.path.isdir(destdir): os.makedirs(destdir) - buf = open(dest, "w") - buf.write(workspace) - buf.close() + with open(dest, "w") as buf: + buf.write(workspace) if not args.quiet: print("Saved to %s." % dest) From 01ec331a0ed1369938bb84fd3a4f88b4725b9730 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 06:23:09 -0500 Subject: [PATCH 43/45] chore(ruff): Manual fixes for cli/convert.py src/tmuxp/cli/convert.py:64:15: TRY002 Create your own exception src/tmuxp/cli/convert.py:64:15: TRY003 Avoid specifying long messages outside the exception class src/tmuxp/cli/convert.py:75:5: SIM102 Use a single `if` statement instead of nested `if` statements src/tmuxp/cli/convert.py:76:9: SIM102 Use a single `if` statement instead of nested `if` statements src/tmuxp/cli/convert.py:81:15: SIM115 Use context handler for opening files --- src/tmuxp/cli/convert.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/tmuxp/cli/convert.py b/src/tmuxp/cli/convert.py index d10e5bd4359..96251cec721 100644 --- a/src/tmuxp/cli/convert.py +++ b/src/tmuxp/cli/convert.py @@ -6,6 +6,7 @@ from tmuxp.config_reader import ConfigReader from tmuxp.workspace.finders import find_workspace_file, get_workspace_dir +from .. import exc from .utils import prompt_yes_no if t.TYPE_CHECKING: @@ -40,6 +41,13 @@ def create_convert_subparser( return parser +class ConvertUnknownFileType(exc.TmuxpException): + def __init__(self, ext: str, *args: object, **kwargs: object) -> None: + return super().__init__( + f"Unknown filetype: {ext} (valid: [.json, .yaml, .yml])" + ) + + def command_convert( workspace_file: t.Union[str, pathlib.Path], answer_yes: bool, @@ -61,7 +69,7 @@ def command_convert( elif ext in [".yaml", ".yml"]: to_filetype = "json" else: - raise Exception(f"Unknown filetype: {ext} (valid: [.json, .yaml, .yml])") + raise ConvertUnknownFileType(ext) configparser = ConfigReader.from_file(workspace_file) newfile = workspace_file.parent / (str(workspace_file.stem) + f".{to_filetype}") @@ -72,13 +80,14 @@ def command_convert( **{"default_flow_style": False} if to_filetype == "yaml" else {}, ) - if not answer_yes: - if prompt_yes_no(f"Convert to <{workspace_file}> to {to_filetype}?"): - if prompt_yes_no("Save workspace to %s?" % newfile): - answer_yes = True + if ( + not answer_yes + and prompt_yes_no(f"Convert to <{workspace_file}> to {to_filetype}?") + and prompt_yes_no("Save workspace to %s?" % newfile) + ): + answer_yes = True if answer_yes: - buf = open(newfile, "w") - buf.write(new_workspace) - buf.close() + with open(newfile, "w") as buf: + buf.write(new_workspace) print(f"New workspace file saved to <{newfile}>.") From 33669feab7a2527338657edd4813b8c10cb3d139 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 06:27:51 -0500 Subject: [PATCH 44/45] chore(ruff): Manual fixes aafig.py docs/_ext/aafig.py:61:23: RUF012 Mutable class attributes should be annotated with `typing.ClassVar` docs/_ext/aafig.py:147:15: TRY003 Avoid specifying long messages outside the exception class docs/_ext/aafig.py:176:29: SIM115 Use context handler for opening files docs/_ext/aafig.py:179:25: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling docs/_ext/aafig.py:179:25: TRY200 Use `raise from` to specify exception cause docs/_ext/aafig.py:193:9: B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling docs/_ext/aafig.py:193:9: TRY200 Use `raise from` to specify exception cause docs/_ext/aafig.py:198:13: SIM115 Use context handler for opening files --- docs/_ext/aafig.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/_ext/aafig.py b/docs/_ext/aafig.py index 70c8b343911..3770613d68e 100644 --- a/docs/_ext/aafig.py +++ b/docs/_ext/aafig.py @@ -12,6 +12,7 @@ """ import logging import posixpath +import typing as t from hashlib import sha1 as sha from os import path @@ -32,7 +33,7 @@ def merge_dict(dst, src): - for (k, v) in src.items(): + for k, v in src.items(): if k not in dst: dst[k] = v return dst @@ -58,7 +59,7 @@ class AafigDirective(images.Image): has_content = True required_arguments = 0 - own_option_spec = { + own_option_spec: t.ClassVar = { "line_width": float, "background": str, "foreground": str, @@ -73,7 +74,7 @@ class AafigDirective(images.Image): def run(self): aafig_options = {} own_options_keys = [self.own_option_spec.keys(), "scale"] - for (k, v) in self.options.items(): + for k, v in self.options.items(): if k in own_options_keys: # convert flags to booleans if v is None: @@ -138,13 +139,18 @@ def render_aafig_images(app, doctree): img["height"] = height +class AafigureNotInstalled(AafigError): + def __init__(self, *args: object, **kwargs: object) -> None: + return super().__init__("aafigure module not installed", *args, **kwargs) + + def render_aafigure(app, text, options): """ Render an ASCII art figure into the requested format output file. """ if aafigure is None: - raise AafigError("aafigure module not installed") + raise AafigureNotInstalled() fname = get_basename(text, options) fname = "{}.{}".format(get_basename(text, options), options["format"]) @@ -173,10 +179,10 @@ def render_aafigure(app, text, options): f = None try: try: - f = open(metadata_fname) - extra = f.read() - except Exception: - raise AafigError() + with open(metadata_fname) as f: + extra = f.read() + except Exception as e: + raise AafigError() from e finally: if f is not None: f.close() @@ -190,14 +196,13 @@ def render_aafigure(app, text, options): (visitor, output) = aafigure.render(text, outfn, options) output.close() except aafigure.UnsupportedFormatError as e: - raise AafigError(str(e)) + raise AafigError(str(e)) from e extra = None if options["format"].lower() == "svg": extra = visitor.get_size_attrs() - f = open(metadata_fname, "w") - f.write(extra) - f.close() + with open(metadata_fname, "w") as f: + f.write(extra) return relfn, outfn, id, extra From 08a9c03514d212a2098458b862586079f556a2aa Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 4 Sep 2023 06:40:29 -0500 Subject: [PATCH 45/45] docs(CHANGES): Note ruff stringency changes --- CHANGES | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES b/CHANGES index bbee3c57d37..4cef97c8a8e 100644 --- a/CHANGES +++ b/CHANGES @@ -19,6 +19,14 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force +### Development + +- Code quality improved via [ruff] rules (#879) + + This includes fixes made by hand alongside ruff's automated fixes. The more + stringent rules include import sorting, and still runs almost instantly + against the whole codebase. + ## tmuxp 1.29.1 (2023-09-02) ### Development