diff --git a/CHANGES b/CHANGES index a911f7aa029..73d61f9a9e0 100644 --- a/CHANGES +++ b/CHANGES @@ -19,6 +19,17 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force +### Development + +#### chore: Implement PEP 563 deferred annotation resolution (#957) + +- Add `from __future__ import annotations` to defer annotation resolution and reduce unnecessary runtime computations during type checking. +- Enable Ruff checks for PEP-compliant annotations: + - [non-pep585-annotation (UP006)](https://docs.astral.sh/ruff/rules/non-pep585-annotation/) + - [non-pep604-annotation (UP007)](https://docs.astral.sh/ruff/rules/non-pep604-annotation/) + +For more details on PEP 563, see: https://peps.python.org/pep-0563/ + ## tmuxp 1.50.1 (2024-12-24) ### Development diff --git a/conftest.py b/conftest.py index 54d0b18bb03..4a22edb1dba 100644 --- a/conftest.py +++ b/conftest.py @@ -8,6 +8,8 @@ https://docs.pytest.org/en/stable/deprecations.html """ +from __future__ import annotations + import logging import os import pathlib @@ -29,7 +31,7 @@ @pytest.fixture(autouse=USING_ZSH, scope="session") -def zshrc(user_path: pathlib.Path) -> t.Optional[pathlib.Path]: +def zshrc(user_path: pathlib.Path) -> pathlib.Path | None: """Quiets ZSH default message. Needs a startup file .zshenv, .zprofile, .zshrc, .zlogin. diff --git a/docs/_ext/aafig.py b/docs/_ext/aafig.py index 987cc398256..ae83db35f01 100644 --- a/docs/_ext/aafig.py +++ b/docs/_ext/aafig.py @@ -12,6 +12,8 @@ :license: BOLA, see LICENSE for details """ +from __future__ import annotations + import locale import logging import posixpath @@ -40,9 +42,9 @@ def merge_dict( - dst: dict[str, t.Optional[str]], - src: dict[str, t.Optional[str]], -) -> dict[str, t.Optional[str]]: + dst: dict[str, str | None], + src: dict[str, str | None], +) -> dict[str, str | None]: for k, v in src.items(): if k not in dst: dst[k] = v @@ -52,7 +54,7 @@ def merge_dict( def get_basename( text: str, options: dict[str, str], - prefix: t.Optional[str] = "aafig", + prefix: str | None = "aafig", ) -> str: options = options.copy() if "format" in options: @@ -105,7 +107,7 @@ def run(self) -> list[nodes.Node]: return [image_node] -def render_aafig_images(app: "Sphinx", doctree: nodes.Node) -> None: +def render_aafig_images(app: Sphinx, doctree: nodes.Node) -> None: format_map = app.builder.config.aafig_format merge_dict(format_map, DEFAULT_FORMATS) if aafigure is None: @@ -157,10 +159,10 @@ def __init__(self, *args: object, **kwargs: object) -> None: def render_aafigure( - app: "Sphinx", + app: Sphinx, text: str, options: dict[str, str], -) -> tuple[str, str, t.Optional[str], t.Optional[str]]: +) -> tuple[str, str, str | None, str | None]: """Render an ASCII art figure into the requested format output file.""" if aafigure is None: raise AafigureNotInstalled @@ -227,7 +229,7 @@ def render_aafigure( return relfn, outfn, None, extra -def setup(app: "Sphinx") -> None: +def setup(app: Sphinx) -> None: app.add_directive("aafig", AafigDirective) app.connect("doctree-read", render_aafig_images) app.add_config_value("aafig_format", DEFAULT_FORMATS, "html") diff --git a/docs/conf.py b/docs/conf.py index 980b8488909..81c72be3457 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,8 @@ # flake8: NOQA: E501 """Sphinx documentation configuration for tmuxp.""" +from __future__ import annotations + import contextlib import inspect import pathlib @@ -74,7 +76,7 @@ html_favicon = "_static/favicon.ico" html_theme = "furo" html_theme_path: list[str] = [] -html_theme_options: dict[str, t.Union[str, list[dict[str, str]]]] = { +html_theme_options: dict[str, str | list[dict[str, str]]] = { "light_logo": "img/tmuxp.svg", "dark_logo": "img/tmuxp.svg", "footer_icons": [ @@ -143,7 +145,7 @@ } -def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]: +def linkcode_resolve(domain: str, info: dict[str, str]) -> None | str: """ Determine the URL corresponding to Python object. @@ -213,7 +215,7 @@ def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]: ) -def remove_tabs_js(app: "Sphinx", exc: Exception) -> None: +def remove_tabs_js(app: Sphinx, exc: Exception) -> None: """Fix for sphinx-inline-tabs#18.""" if app.builder.format == "html" and not exc: tabs_js = pathlib.Path(app.builder.outdir) / "_static" / "tabs.js" @@ -221,6 +223,6 @@ def remove_tabs_js(app: "Sphinx", exc: Exception) -> None: tabs_js.unlink() # When python 3.7 deprecated, use missing_ok=True -def setup(app: "Sphinx") -> None: +def setup(app: Sphinx) -> None: """Sphinx setup hook.""" app.connect("build-finished", remove_tabs_js) diff --git a/pyproject.toml b/pyproject.toml index 520c54aaf7a..75dd57d2174 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,6 +149,9 @@ exclude_lines = [ "raise NotImplementedError", "if __name__ == .__main__.:", "def parse_args", + "from __future__ import annotations", + "if TYPE_CHECKING:", + "if t.TYPE_CHECKING:", ] [tool.mypy] @@ -192,16 +195,25 @@ select = [ "PERF", # Perflint "RUF", # Ruff-specific rules "D", # pydocstyle + "FA100", # future annotations ] ignore = [ "COM812", # missing trailing comma, ruff format conflict ] +extend-safe-fixes = [ + "UP006", + "UP007", +] +pyupgrade.keep-runtime-typing = false [tool.ruff.lint.isort] known-first-party = [ "tmuxp", ] combine-as-imports = true +required-imports = [ + "from __future__ import annotations", +] [tool.ruff.lint.pydocstyle] convention = "numpy" diff --git a/src/tmuxp/__about__.py b/src/tmuxp/__about__.py index 0968fbb7c94..1e9b2740885 100644 --- a/src/tmuxp/__about__.py +++ b/src/tmuxp/__about__.py @@ -1,5 +1,7 @@ """Metadata for tmuxp package.""" +from __future__ import annotations + __title__ = "tmuxp" __package_name__ = "tmuxp" __version__ = "1.50.1" diff --git a/src/tmuxp/__init__.py b/src/tmuxp/__init__.py index a7d3e623e0f..ac9f8422e78 100644 --- a/src/tmuxp/__init__.py +++ b/src/tmuxp/__init__.py @@ -5,6 +5,8 @@ :license: MIT, see LICENSE for details """ +from __future__ import annotations + from . import cli, util from .__about__ import ( __author__, diff --git a/src/tmuxp/_internal/config_reader.py b/src/tmuxp/_internal/config_reader.py index 6cd862edb96..f7db20e48de 100644 --- a/src/tmuxp/_internal/config_reader.py +++ b/src/tmuxp/_internal/config_reader.py @@ -1,5 +1,7 @@ """Configuration parser for YAML and JSON files.""" +from __future__ import annotations + import json import pathlib import typing as t @@ -24,11 +26,11 @@ class ConfigReader: '{\n "session_name": "my session"\n}' """ - def __init__(self, content: "RawConfigData") -> None: + def __init__(self, content: RawConfigData) -> None: self.content = content @staticmethod - def _load(fmt: "FormatLiteral", content: str) -> dict[str, t.Any]: + def _load(fmt: FormatLiteral, content: str) -> dict[str, t.Any]: """Load raw config data and directly return it. >>> ConfigReader._load("json", '{ "session_name": "my session" }') @@ -51,7 +53,7 @@ def _load(fmt: "FormatLiteral", content: str) -> dict[str, t.Any]: raise NotImplementedError(msg) @classmethod - def load(cls, fmt: "FormatLiteral", content: str) -> "ConfigReader": + def load(cls, fmt: FormatLiteral, content: str) -> ConfigReader: """Load raw config data into a ConfigReader instance (to dump later). >>> cfg = ConfigReader.load("json", '{ "session_name": "my session" }') @@ -120,7 +122,7 @@ def _from_file(cls, path: pathlib.Path) -> dict[str, t.Any]: ) @classmethod - def from_file(cls, path: pathlib.Path) -> "ConfigReader": + def from_file(cls, path: pathlib.Path) -> ConfigReader: r"""Load data from file path. **YAML file** @@ -161,8 +163,8 @@ def from_file(cls, path: pathlib.Path) -> "ConfigReader": @staticmethod def _dump( - fmt: "FormatLiteral", - content: "RawConfigData", + fmt: FormatLiteral, + content: RawConfigData, indent: int = 2, **kwargs: t.Any, ) -> str: @@ -189,7 +191,7 @@ def _dump( msg = f"{fmt} not supported in config" raise NotImplementedError(msg) - def dump(self, fmt: "FormatLiteral", indent: int = 2, **kwargs: t.Any) -> str: + def dump(self, fmt: FormatLiteral, indent: int = 2, **kwargs: t.Any) -> str: r"""Dump via ConfigReader instance. >>> cfg = ConfigReader({ "session_name": "my session" }) diff --git a/src/tmuxp/_internal/types.py b/src/tmuxp/_internal/types.py index aa2d9444db2..c66d88956cf 100644 --- a/src/tmuxp/_internal/types.py +++ b/src/tmuxp/_internal/types.py @@ -10,6 +10,8 @@ ... """ +from __future__ import annotations + from typing_extensions import NotRequired, TypedDict diff --git a/src/tmuxp/cli/__init__.py b/src/tmuxp/cli/__init__.py index f285cb7d9cb..8af6a50547f 100644 --- a/src/tmuxp/cli/__init__.py +++ b/src/tmuxp/cli/__init__.py @@ -1,9 +1,10 @@ """CLI utilities for tmuxp.""" +from __future__ import annotations + import argparse import logging import os -import pathlib import sys import typing as t @@ -32,6 +33,8 @@ logger = logging.getLogger(__name__) if t.TYPE_CHECKING: + import pathlib + from typing_extensions import TypeAlias CLIVerbosity: TypeAlias = t.Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] @@ -109,16 +112,16 @@ def create_parser() -> argparse.ArgumentParser: class CLINamespace(argparse.Namespace): """Typed :class:`argparse.Namespace` for tmuxp root-level CLI.""" - log_level: "CLIVerbosity" - subparser_name: "CLISubparserName" - import_subparser_name: t.Optional["CLIImportSubparserName"] + log_level: CLIVerbosity + subparser_name: CLISubparserName + import_subparser_name: CLIImportSubparserName | None version: bool ns = CLINamespace() -def cli(_args: t.Optional[list[str]] = None) -> None: +def cli(_args: list[str] | None = None) -> None: """Manage tmux sessions. Pass the "--help" argument to any command to see detailed help. diff --git a/src/tmuxp/cli/convert.py b/src/tmuxp/cli/convert.py index 0d9ee5173d8..d2f13087d72 100644 --- a/src/tmuxp/cli/convert.py +++ b/src/tmuxp/cli/convert.py @@ -1,6 +1,7 @@ """CLI for ``tmuxp convert`` subcommand.""" -import argparse +from __future__ import annotations + import locale import os import pathlib @@ -13,6 +14,8 @@ from .utils import prompt_yes_no if t.TYPE_CHECKING: + import argparse + AllowedFileTypes = t.Literal["json", "yaml"] @@ -53,9 +56,9 @@ def __init__(self, ext: str, *args: object, **kwargs: object) -> None: def command_convert( - workspace_file: t.Union[str, pathlib.Path], + workspace_file: str | pathlib.Path, answer_yes: bool, - parser: t.Optional[argparse.ArgumentParser] = None, + parser: argparse.ArgumentParser | None = None, ) -> None: """Entrypoint for ``tmuxp convert`` convert a tmuxp config between JSON and YAML.""" workspace_file = find_workspace_file( @@ -95,4 +98,3 @@ def command_convert( if answer_yes: with open(newfile, "w", encoding=locale.getpreferredencoding(False)) as buf: buf.write(new_workspace) - print(f"New workspace file saved to <{newfile}>.") diff --git a/src/tmuxp/cli/debug_info.py b/src/tmuxp/cli/debug_info.py index f8908a5ba25..fdd55c83fdc 100644 --- a/src/tmuxp/cli/debug_info.py +++ b/src/tmuxp/cli/debug_info.py @@ -1,6 +1,7 @@ """CLI for ``tmuxp debug-info`` subcommand.""" -import argparse +from __future__ import annotations + import os import pathlib import platform @@ -16,6 +17,9 @@ from .utils import tmuxp_echo +if t.TYPE_CHECKING: + import argparse + tmuxp_path = pathlib.Path(__file__).parent.parent @@ -27,7 +31,7 @@ def create_debug_info_subparser( def command_debug_info( - parser: t.Optional[argparse.ArgumentParser] = None, + parser: argparse.ArgumentParser | None = None, ) -> None: """Entrypoint for ``tmuxp debug-info`` to print debug info to submit with issues.""" diff --git a/src/tmuxp/cli/edit.py b/src/tmuxp/cli/edit.py index 52bdfaf00b0..075ca201dd7 100644 --- a/src/tmuxp/cli/edit.py +++ b/src/tmuxp/cli/edit.py @@ -1,13 +1,17 @@ """CLI for ``tmuxp edit`` subcommand.""" -import argparse +from __future__ import annotations + import os -import pathlib import subprocess import typing as t from tmuxp.workspace.finders import find_workspace_file +if t.TYPE_CHECKING: + import argparse + import pathlib + def create_edit_subparser( parser: argparse.ArgumentParser, @@ -23,8 +27,8 @@ def create_edit_subparser( def command_edit( - workspace_file: t.Union[str, pathlib.Path], - parser: t.Optional[argparse.ArgumentParser] = None, + workspace_file: str | pathlib.Path, + parser: argparse.ArgumentParser | None = None, ) -> None: """Entrypoint for ``tmuxp edit``, open tmuxp workspace file in system editor.""" workspace_file = find_workspace_file(workspace_file) diff --git a/src/tmuxp/cli/freeze.py b/src/tmuxp/cli/freeze.py index b6d4e276173..772a60e6fac 100644 --- a/src/tmuxp/cli/freeze.py +++ b/src/tmuxp/cli/freeze.py @@ -1,5 +1,7 @@ """CLI for ``tmuxp freeze`` subcommand.""" +from __future__ import annotations + import argparse import locale import os @@ -27,13 +29,13 @@ class CLIFreezeNamespace(argparse.Namespace): """Typed :class:`argparse.Namespace` for tmuxp freeze command.""" session_name: str - socket_name: t.Optional[str] - socket_path: t.Optional[str] - workspace_format: t.Optional["CLIOutputFormatLiteral"] - save_to: t.Optional[str] - answer_yes: t.Optional[bool] - quiet: t.Optional[bool] - force: t.Optional[bool] + socket_name: str | None + socket_path: str | None + workspace_format: CLIOutputFormatLiteral | None + save_to: str | None + answer_yes: bool | None + quiet: bool | None + force: bool | None def create_freeze_subparser( @@ -97,7 +99,7 @@ def create_freeze_subparser( def command_freeze( args: CLIFreezeNamespace, - parser: t.Optional[argparse.ArgumentParser] = None, + parser: argparse.ArgumentParser | None = None, ) -> None: """Entrypoint for ``tmuxp freeze``, snapshot a tmux session into a tmuxp workspace. @@ -114,8 +116,7 @@ def command_freeze( if not session: raise exc.SessionNotFound - except TmuxpException as e: - print(e) + except TmuxpException: return frozen_workspace = freezer.freeze(session) @@ -123,11 +124,7 @@ def command_freeze( configparser = ConfigReader(workspace) if not args.quiet: - print( - "---------------------------------------------------------------" - "\n" - "Freeze does its best to snapshot live tmux sessions.\n", - ) + pass if not ( args.answer_yes or prompt_yes_no( @@ -135,11 +132,7 @@ def command_freeze( ) ): if not args.quiet: - print( - "tmuxp has examples in JSON and YAML format at " - "\n" - "View tmuxp docs at .", - ) + pass sys.exit() dest = args.save_to @@ -158,7 +151,6 @@ def command_freeze( default=save_to, ) if not args.force and os.path.exists(dest_prompt): - print(f"{dest_prompt} exists. Pick a new filename.") continue dest = dest_prompt @@ -167,14 +159,14 @@ def command_freeze( valid_workspace_formats: list[CLIOutputFormatLiteral] = ["json", "yaml"] - def is_valid_ext(stem: t.Optional[str]) -> "TypeGuard[CLIOutputFormatLiteral]": + def is_valid_ext(stem: str | None) -> TypeGuard[CLIOutputFormatLiteral]: return stem in valid_workspace_formats if not is_valid_ext(workspace_format): def extract_workspace_format( val: str, - ) -> t.Optional["CLIOutputFormatLiteral"]: + ) -> CLIOutputFormatLiteral | None: suffix = pathlib.Path(val).suffix if isinstance(suffix, str): suffix = suffix.lower().lstrip(".") @@ -212,4 +204,4 @@ def extract_workspace_format( buf.write(workspace) if not args.quiet: - print(f"Saved to {dest}.") + pass diff --git a/src/tmuxp/cli/import_config.py b/src/tmuxp/cli/import_config.py index 1f4127b61d6..a014e1558e3 100644 --- a/src/tmuxp/cli/import_config.py +++ b/src/tmuxp/cli/import_config.py @@ -1,6 +1,7 @@ """CLI for ``tmuxp shell`` subcommand.""" -import argparse +from __future__ import annotations + import locale import os import pathlib @@ -13,6 +14,9 @@ from .utils import prompt, prompt_choices, prompt_yes_no, tmuxp_echo +if t.TYPE_CHECKING: + import argparse + def get_tmuxinator_dir() -> pathlib.Path: """Return tmuxinator configuration directory. @@ -136,7 +140,7 @@ def __call__(self, workspace_dict: dict[str, t.Any]) -> dict[str, t.Any]: def import_config( workspace_file: str, importfunc: ImportConfigFn, - parser: t.Optional[argparse.ArgumentParser] = None, + parser: argparse.ArgumentParser | None = None, ) -> None: """Import a configuration from a workspace_file.""" existing_workspace_file = ConfigReader._from_file(pathlib.Path(workspace_file)) @@ -189,7 +193,7 @@ def import_config( def command_import_tmuxinator( workspace_file: str, - parser: t.Optional[argparse.ArgumentParser] = None, + parser: argparse.ArgumentParser | None = None, ) -> None: """Entrypoint for ``tmuxp import tmuxinator`` subcommand. @@ -205,7 +209,7 @@ def command_import_tmuxinator( def command_import_teamocil( workspace_file: str, - parser: t.Optional[argparse.ArgumentParser] = None, + parser: argparse.ArgumentParser | None = None, ) -> None: """Entrypoint for ``tmuxp import teamocil`` subcommand. diff --git a/src/tmuxp/cli/load.py b/src/tmuxp/cli/load.py index 551d9ecf294..fa65e1b8f5a 100644 --- a/src/tmuxp/cli/load.py +++ b/src/tmuxp/cli/load.py @@ -1,5 +1,7 @@ """CLI for ``tmuxp load`` subcommand.""" +from __future__ import annotations + import argparse import importlib import logging @@ -10,11 +12,9 @@ import typing as t from libtmux.server import Server -from libtmux.session import Session from tmuxp import exc, log, util from tmuxp._internal import config_reader -from tmuxp.types import StrPath from tmuxp.workspace import loader from tmuxp.workspace.builder import WorkspaceBuilder from tmuxp.workspace.finders import find_workspace_file, get_workspace_dir @@ -22,29 +22,32 @@ from .utils import prompt_choices, prompt_yes_no, style, tmuxp_echo if t.TYPE_CHECKING: + from libtmux.session import Session from typing_extensions import NotRequired, TypeAlias, TypedDict + from tmuxp.types import StrPath + CLIColorsLiteral: TypeAlias = t.Literal[56, 88] class OptionOverrides(TypedDict): """Optional argument overrides for tmuxp load.""" detached: NotRequired[bool] - new_session_name: NotRequired[t.Optional[str]] + new_session_name: NotRequired[str | None] class CLILoadNamespace(argparse.Namespace): """Typed :class:`argparse.Namespace` for tmuxp load command.""" workspace_files: list[str] - socket_name: t.Optional[str] - socket_path: t.Optional[str] - tmux_config_file: t.Optional[str] - new_session_name: t.Optional[str] - answer_yes: t.Optional[bool] - append: t.Optional[bool] - colors: t.Optional["CLIColorsLiteral"] - log_file: t.Optional[str] + socket_name: str | None + socket_path: str | None + tmux_config_file: str | None + new_session_name: str | None + answer_yes: bool | None + append: bool | None + colors: CLIColorsLiteral | None + log_file: str | None def load_plugins(session_config: dict[str, t.Any]) -> list[t.Any]: @@ -115,7 +118,7 @@ def _reattach(builder: WorkspaceBuilder) -> None: plugin.reattach(builder.session) proc = builder.session.cmd("display-message", "-p", "'#S'") for line in proc.stdout: - print(line) + print(line) # NOQA: T201 RUF100 if "TMUX" in os.environ: builder.session.switch_client() @@ -159,7 +162,7 @@ def _load_detached(builder: WorkspaceBuilder) -> None: assert builder.session is not None - print("Session created in detached state.") + print("Session created in detached state.") # NOQA: T201 RUF100 def _load_append_windows_to_current_session(builder: WorkspaceBuilder) -> None: @@ -191,15 +194,15 @@ def _setup_plugins(builder: WorkspaceBuilder) -> Session: def load_workspace( workspace_file: StrPath, - socket_name: t.Optional[str] = None, + socket_name: str | None = None, socket_path: None = None, - tmux_config_file: t.Optional[str] = None, - new_session_name: t.Optional[str] = None, - colors: t.Optional[int] = None, + tmux_config_file: str | None = None, + new_session_name: str | None = None, + colors: int | None = None, detached: bool = False, answer_yes: bool = False, append: bool = False, -) -> t.Optional[Session]: +) -> Session | None: """Entrypoint for ``tmuxp load``, load a tmuxp "workspace" session via config file. Parameters @@ -507,7 +510,7 @@ def create_load_subparser(parser: argparse.ArgumentParser) -> argparse.ArgumentP def command_load( args: CLILoadNamespace, - parser: t.Optional[argparse.ArgumentParser] = None, + parser: argparse.ArgumentParser | None = None, ) -> None: """Load a tmux workspace from each WORKSPACE_FILE. diff --git a/src/tmuxp/cli/ls.py b/src/tmuxp/cli/ls.py index e5c0defcdc5..e1a8c7a6109 100644 --- a/src/tmuxp/cli/ls.py +++ b/src/tmuxp/cli/ls.py @@ -1,12 +1,16 @@ """CLI for ``tmuxp ls`` subcommand.""" -import argparse +from __future__ import annotations + import os import typing as t from tmuxp.workspace.constants import VALID_WORKSPACE_DIR_FILE_EXTENSIONS from tmuxp.workspace.finders import get_workspace_dir +if t.TYPE_CHECKING: + import argparse + def create_ls_subparser( parser: argparse.ArgumentParser, @@ -16,7 +20,7 @@ def create_ls_subparser( def command_ls( - parser: t.Optional[argparse.ArgumentParser] = None, + parser: argparse.ArgumentParser | None = None, ) -> None: """Entrypoint for ``tmuxp ls`` subcommand.""" tmuxp_dir = get_workspace_dir() @@ -25,4 +29,4 @@ def command_ls( stem, ext = os.path.splitext(f) if os.path.isdir(f) or ext not in VALID_WORKSPACE_DIR_FILE_EXTENSIONS: continue - print(stem) + print(stem) # NOQA: T201 RUF100 diff --git a/src/tmuxp/cli/shell.py b/src/tmuxp/cli/shell.py index eee241a0322..c3b005c287a 100644 --- a/src/tmuxp/cli/shell.py +++ b/src/tmuxp/cli/shell.py @@ -1,5 +1,7 @@ """CLI for ``tmuxp shell`` subcommand.""" +from __future__ import annotations + import argparse import os import pathlib @@ -29,13 +31,13 @@ class CLIShellNamespace(argparse.Namespace): """Typed :class:`argparse.Namespace` for tmuxp shell command.""" session_name: str - socket_name: t.Optional[str] - socket_path: t.Optional[str] - colors: t.Optional["CLIColorsLiteral"] - log_file: t.Optional[str] - window_name: t.Optional[str] - command: t.Optional[str] - shell: t.Optional["CLIShellLiteral"] + socket_name: str | None + socket_path: str | None + colors: CLIColorsLiteral | None + log_file: str | None + window_name: str | None + command: str | None + shell: CLIShellLiteral | None use_pythonrc: bool use_vi_mode: bool @@ -147,7 +149,7 @@ def create_shell_subparser(parser: argparse.ArgumentParser) -> argparse.Argument def command_shell( args: CLIShellNamespace, - parser: t.Optional[argparse.ArgumentParser] = None, + parser: argparse.ArgumentParser | None = None, ) -> None: """Entrypoint for ``tmuxp shell`` for tmux server, session, window and pane. diff --git a/src/tmuxp/cli/utils.py b/src/tmuxp/cli/utils.py index c101542c407..dbb40b08b75 100644 --- a/src/tmuxp/cli/utils.py +++ b/src/tmuxp/cli/utils.py @@ -1,13 +1,16 @@ """CLI utility helpers for tmuxp.""" +from __future__ import annotations + import logging import re import typing as t -from collections.abc import Callable, Sequence from tmuxp import log if t.TYPE_CHECKING: + from collections.abc import Callable, Sequence + from typing_extensions import TypeAlias CLIColour: TypeAlias = t.Union[int, tuple[int, int, int], str] @@ -17,7 +20,7 @@ def tmuxp_echo( - message: t.Optional[str] = None, + message: str | None = None, log_level: str = "INFO", style_log: bool = False, ) -> None: @@ -30,13 +33,13 @@ def tmuxp_echo( else: logger.log(log.LOG_LEVELS[log_level], unstyle(message)) - print(message) + print(message) # NOQA: T201 RUF100 def prompt( name: str, - default: t.Optional[str] = None, - value_proc: t.Optional[Callable[[str], str]] = None, + default: str | None = None, + value_proc: Callable[[str], str] | None = None, ) -> str: """Return user input from command line. @@ -77,8 +80,8 @@ def prompt( def prompt_bool( name: str, default: bool = False, - yes_choices: t.Optional[Sequence[t.Any]] = None, - no_choices: t.Optional[Sequence[t.Any]] = None, + yes_choices: Sequence[t.Any] | None = None, + no_choices: Sequence[t.Any] | None = None, ) -> bool: """Return True / False by prompting user input from command line. @@ -127,10 +130,10 @@ def prompt_yes_no(name: str, default: bool = True) -> bool: def prompt_choices( name: str, - choices: t.Union[list[str], tuple[str, str]], - default: t.Optional[str] = None, + choices: list[str] | tuple[str, str], + default: str | None = None, no_choice: Sequence[str] = ("none",), -) -> t.Optional[str]: +) -> str | None: """Return user input from command line from set of provided choices. Parameters @@ -202,7 +205,7 @@ def strip_ansi(value: str) -> str: def _interpret_color( - color: t.Union[int, tuple[int, int, int], str], + color: int | tuple[int, int, int] | str, offset: int = 0, ) -> str: if isinstance(color, int): @@ -218,22 +221,22 @@ def _interpret_color( class UnknownStyleColor(Exception): """Raised when encountering an unknown terminal style color.""" - def __init__(self, color: "CLIColour", *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["CLIColour"] = None, - bg: t.Optional["CLIColour"] = None, - bold: t.Optional[bool] = None, - dim: t.Optional[bool] = None, - underline: t.Optional[bool] = None, - overline: t.Optional[bool] = None, - italic: t.Optional[bool] = None, - blink: t.Optional[bool] = None, - reverse: t.Optional[bool] = None, - strikethrough: t.Optional[bool] = None, + fg: CLIColour | None = None, + bg: CLIColour | None = None, + bold: bool | None = None, + dim: bool | None = None, + underline: bool | None = None, + overline: bool | None = None, + italic: bool | None = None, + blink: bool | None = None, + reverse: bool | None = None, + strikethrough: bool | None = None, reset: bool = True, ) -> str: """Credit: click.""" diff --git a/src/tmuxp/exc.py b/src/tmuxp/exc.py index 118ba00bcd2..525599270f9 100644 --- a/src/tmuxp/exc.py +++ b/src/tmuxp/exc.py @@ -1,6 +1,6 @@ """Exceptions for tmuxp.""" -import typing as t +from __future__ import annotations from libtmux._internal.query_list import ObjectDoesNotExist @@ -20,7 +20,7 @@ class SessionNotFound(TmuxpException): def __init__( self, - session_target: t.Optional[str] = None, + session_target: str | None = None, *args: object, **kwargs: object, ) -> None: @@ -35,7 +35,7 @@ class WindowNotFound(TmuxpException): def __init__( self, - window_target: t.Optional[str] = None, + window_target: str | None = None, *args: object, **kwargs: object, ) -> None: @@ -50,7 +50,7 @@ class PaneNotFound(TmuxpException): def __init__( self, - pane_target: t.Optional[str] = None, + pane_target: str | None = None, *args: object, **kwargs: object, ) -> None: @@ -115,7 +115,7 @@ def __init__( self, returncode: int, cmd: str, - output: t.Optional[str] = None, + output: str | None = None, ) -> None: self.returncode = returncode self.cmd = cmd diff --git a/src/tmuxp/log.py b/src/tmuxp/log.py index 57855dcabba..613b140a659 100644 --- a/src/tmuxp/log.py +++ b/src/tmuxp/log.py @@ -1,6 +1,8 @@ #!/usr/bin/env python """Log utilities for tmuxp.""" +from __future__ import annotations + import logging import time import typing as t @@ -26,7 +28,7 @@ def setup_logger( - logger: t.Optional[logging.Logger] = None, + logger: logging.Logger | None = None, level: str = "INFO", ) -> None: """Configure tmuxp's logging for CLI use. @@ -137,7 +139,7 @@ def format(self, record: logging.LogRecord) -> str: def debug_log_template( self: type[logging.Formatter], record: logging.LogRecord, - stylized: t.Optional[bool] = False, + stylized: bool | None = False, **kwargs: t.Any, ) -> str: """ diff --git a/src/tmuxp/plugin.py b/src/tmuxp/plugin.py index 9920e873f42..4873112c8ba 100644 --- a/src/tmuxp/plugin.py +++ b/src/tmuxp/plugin.py @@ -1,5 +1,7 @@ """Plugin system for tmuxp.""" +from __future__ import annotations + import typing as t import libtmux @@ -38,10 +40,10 @@ class VersionConstraints(TypedDict): """Version constraints mapping for a tmuxp plugin.""" - version: t.Union[Version, str] + version: Version | str vmin: str - vmax: t.Optional[str] - incompatible: list[t.Union[t.Any, str]] + vmax: str | None + incompatible: list[t.Any | str] class TmuxpPluginVersionConstraints(TypedDict): """Version constraints for a tmuxp plugin.""" @@ -56,17 +58,17 @@ class Config(t.TypedDict): plugin_name: str tmux_min_version: str - tmux_max_version: t.Optional[str] - tmux_version_incompatible: t.Optional[list[str]] + tmux_max_version: str | None + tmux_version_incompatible: list[str] | None libtmux_min_version: str - libtmux_max_version: t.Optional[str] - libtmux_version_incompatible: t.Optional[list[str]] + libtmux_max_version: str | None + libtmux_version_incompatible: list[str] | None tmuxp_min_version: str - tmuxp_max_version: t.Optional[str] - tmuxp_version_incompatible: t.Optional[list[str]] + tmuxp_max_version: str | None + tmuxp_version_incompatible: list[str] | None -DEFAULT_CONFIG: "Config" = { +DEFAULT_CONFIG: Config = { "plugin_name": "tmuxp-plugin", "tmux_min_version": TMUX_MIN_VERSION, "tmux_max_version": TMUX_MAX_VERSION, @@ -80,15 +82,15 @@ class Config(t.TypedDict): } -def validate_plugin_config(config: "PluginConfigSchema") -> "TypeGuard[Config]": +def validate_plugin_config(config: PluginConfigSchema) -> TypeGuard[Config]: """Return True if tmuxp plugin configuration valid, also upcasts.""" return isinstance(config, dict) def setup_plugin_config( - config: "PluginConfigSchema", - default_config: "Config" = DEFAULT_CONFIG, -) -> "Config": + config: PluginConfigSchema, + default_config: Config = DEFAULT_CONFIG, +) -> Config: """Initialize tmuxp plugin configuration.""" new_config = config.copy() for default_key, default_value in default_config.items(): @@ -103,7 +105,7 @@ def setup_plugin_config( class TmuxpPlugin: """Base class for a tmuxp plugin.""" - def __init__(self, **kwargs: "Unpack[PluginConfigSchema]") -> None: + def __init__(self, **kwargs: Unpack[PluginConfigSchema]) -> None: """ Initialize plugin. @@ -197,10 +199,10 @@ def _version_check(self) -> None: def _pass_version_check( self, - version: t.Union[str, Version], + version: str | Version, vmin: str, - vmax: t.Optional[str], - incompatible: list[t.Union[t.Any, str]], + vmax: str | None, + incompatible: list[t.Any | str], ) -> bool: """Provide affirmative if version compatibility is correct.""" if vmin and version < Version(vmin): @@ -209,7 +211,7 @@ def _pass_version_check( return False return version not in incompatible - def before_workspace_builder(self, session: "Session") -> None: + def before_workspace_builder(self, session: Session) -> None: """ Provide a session hook previous to creating the workspace. @@ -222,7 +224,7 @@ def before_workspace_builder(self, session: "Session") -> None: session to hook into """ - def on_window_create(self, window: "Window") -> None: + def on_window_create(self, window: Window) -> None: """ Provide a window hook previous to doing anything with a window. @@ -234,7 +236,7 @@ def on_window_create(self, window: "Window") -> None: window to hook into """ - def after_window_finished(self, window: "Window") -> None: + def after_window_finished(self, window: Window) -> None: """ Provide a window hook after creating the window. @@ -248,7 +250,7 @@ def after_window_finished(self, window: "Window") -> None: window to hook into """ - def before_script(self, session: "Session") -> None: + def before_script(self, session: Session) -> None: """ Provide a session hook after the workspace has been built. @@ -276,7 +278,7 @@ def before_script(self, session: "Session") -> None: session to hook into """ - def reattach(self, session: "Session") -> None: + def reattach(self, session: Session) -> None: """ Provide a session hook before reattaching to the session. diff --git a/src/tmuxp/shell.py b/src/tmuxp/shell.py index bae2a203fa4..78f9857ca25 100644 --- a/src/tmuxp/shell.py +++ b/src/tmuxp/shell.py @@ -1,15 +1,17 @@ # mypy: allow-untyped-calls """Utility and helper methods for tmuxp.""" +from __future__ import annotations + import logging import os import pathlib import typing as t -from collections.abc import Callable logger = logging.getLogger(__name__) if t.TYPE_CHECKING: + from collections.abc import Callable from types import ModuleType from libtmux.pane import Pane @@ -31,10 +33,10 @@ class LaunchOptionalImports(TypedDict): """tmuxp shell optional imports.""" - server: NotRequired["Server"] - session: NotRequired["Session"] - window: NotRequired["Window"] - pane: NotRequired["Pane"] + server: NotRequired[Server] + session: NotRequired[Session] + window: NotRequired[Window] + pane: NotRequired[Pane] class LaunchImports(t.TypedDict): """tmuxp shell launch import mapping.""" @@ -44,10 +46,10 @@ class LaunchImports(t.TypedDict): Session: type[Session] Window: type[Window] Pane: type[Pane] - server: t.Optional["Server"] - session: t.Optional["Session"] - window: t.Optional["Window"] - pane: t.Optional["Pane"] + server: Server | None + session: Session | None + window: Window | None + pane: Pane | None def has_ipython() -> bool: @@ -100,7 +102,7 @@ def has_bpython() -> bool: return True -def detect_best_shell() -> "CLIShellLiteral": +def detect_best_shell() -> CLIShellLiteral: """Return the best, most feature-rich shell available.""" if has_ptipython(): return "ptipython" @@ -114,8 +116,8 @@ def detect_best_shell() -> "CLIShellLiteral": def get_bpython( - options: "LaunchOptionalImports", - extra_args: t.Optional[dict[str, t.Any]] = None, + options: LaunchOptionalImports, + extra_args: dict[str, t.Any] | None = None, ) -> Callable[[], None]: """Return bpython shell.""" if extra_args is None: @@ -140,7 +142,7 @@ def get_ipython_arguments() -> list[str]: def get_ipython( - options: "LaunchOptionalImports", + options: LaunchOptionalImports, **extra_args: dict[str, t.Any], ) -> t.Any: """Return ipython shell.""" @@ -168,7 +170,7 @@ def launch_ipython() -> None: return launch_ipython -def get_ptpython(options: "LaunchOptionalImports", vi_mode: bool = False) -> t.Any: +def get_ptpython(options: LaunchOptionalImports, vi_mode: bool = False) -> t.Any: """Return ptpython shell.""" try: from ptpython.repl import embed, run_config @@ -188,7 +190,7 @@ def launch_ptpython() -> None: return launch_ptpython -def get_ptipython(options: "LaunchOptionalImports", vi_mode: bool = False) -> t.Any: +def get_ptipython(options: LaunchOptionalImports, vi_mode: bool = False) -> t.Any: """Based on django-extensions. Run renamed to launch, get_imported_objects renamed to get_launch_args @@ -214,7 +216,7 @@ def launch_ptipython() -> None: return launch_ptipython -def get_launch_args(**kwargs: "Unpack[LaunchOptionalImports]") -> "LaunchImports": +def get_launch_args(**kwargs: Unpack[LaunchOptionalImports]) -> LaunchImports: """Return tmuxp shell launch arguments, counting for overrides.""" import libtmux from libtmux.pane import Pane @@ -235,7 +237,7 @@ def get_launch_args(**kwargs: "Unpack[LaunchOptionalImports]") -> "LaunchImports } -def get_code(use_pythonrc: bool, imported_objects: "LaunchImports") -> t.Any: +def get_code(use_pythonrc: bool, imported_objects: LaunchImports) -> t.Any: """Launch basic python shell via :mod:`code`.""" import code @@ -290,10 +292,10 @@ def launch_code() -> None: def launch( - shell: t.Optional["CLIShellLiteral"] = "best", + shell: CLIShellLiteral | None = "best", use_pythonrc: bool = False, use_vi_mode: bool = False, - **kwargs: "Unpack[LaunchOptionalImports]", + **kwargs: Unpack[LaunchOptionalImports], ) -> None: """Launch interactive libtmux shell for tmuxp shell.""" # Also allowing passing shell='code' to force using code.interact diff --git a/src/tmuxp/types.py b/src/tmuxp/types.py index d94328dfb3c..4d267ae2eeb 100644 --- a/src/tmuxp/types.py +++ b/src/tmuxp/types.py @@ -7,10 +7,12 @@ .. _typeshed's: https://github.com/python/typeshed/blob/9687d5/stdlib/_typeshed/__init__.pyi#L98 """ # E501 -from typing import TYPE_CHECKING, Union +from __future__ import annotations -if TYPE_CHECKING: +import typing as t + +if t.TYPE_CHECKING: from os import PathLike -StrPath = Union[str, "PathLike[str]"] +StrPath = t.Union[str, "PathLike[str]"] """:class:`os.PathLike` or :class:`str`""" diff --git a/src/tmuxp/util.py b/src/tmuxp/util.py index 3efc5a900d0..b81fdb6e7e2 100644 --- a/src/tmuxp/util.py +++ b/src/tmuxp/util.py @@ -1,8 +1,9 @@ """Utility and helper methods for tmuxp.""" +from __future__ import annotations + import logging import os -import pathlib import shlex import subprocess import sys @@ -13,6 +14,8 @@ from . import exc if t.TYPE_CHECKING: + import pathlib + from libtmux.pane import Pane from libtmux.server import Server from libtmux.session import Session @@ -24,8 +27,8 @@ def run_before_script( - script_file: t.Union[str, pathlib.Path], - cwd: t.Optional[pathlib.Path] = None, + script_file: str | pathlib.Path, + cwd: pathlib.Path | None = None, ) -> int: """Execute a shell script, wraps :meth:`subprocess.check_call()` in a try/catch.""" try: @@ -75,7 +78,7 @@ def oh_my_zsh_auto_title() -> None: or os.environ.get("DISABLE_AUTO_TITLE") == "false" ) ): - print( + print( # NOQA: T201 RUF100 "Please set:\n\n" "\texport DISABLE_AUTO_TITLE='true'\n\n" "in ~/.zshrc or where your zsh profile is stored.\n" @@ -85,7 +88,7 @@ def oh_my_zsh_auto_title() -> None: ) -def get_current_pane(server: "Server") -> t.Optional["Pane"]: +def get_current_pane(server: Server) -> Pane | None: """Return Pane if one found in env.""" if os.getenv("TMUX_PANE") is not None: try: @@ -96,10 +99,10 @@ def get_current_pane(server: "Server") -> t.Optional["Pane"]: def get_session( - server: "Server", - session_name: t.Optional[str] = None, - current_pane: t.Optional["Pane"] = None, -) -> "Session": + server: Server, + session_name: str | None = None, + current_pane: Pane | None = None, +) -> Session: """Get tmux session for server by session name, respects current pane, if passed.""" try: if session_name: @@ -123,10 +126,10 @@ def get_session( def get_window( - session: "Session", - window_name: t.Optional[str] = None, - current_pane: t.Optional["Pane"] = None, -) -> "Window": + session: Session, + window_name: str | None = None, + current_pane: Pane | None = None, +) -> Window: """Get tmux window for server by window name, respects current pane, if passed.""" try: if window_name: @@ -146,7 +149,7 @@ def get_window( return window -def get_pane(window: "Window", current_pane: t.Optional["Pane"] = None) -> "Pane": +def get_pane(window: Window, current_pane: Pane | None = None) -> Pane: """Get tmux pane for server by pane name, respects current pane, if passed.""" pane = None try: @@ -155,7 +158,7 @@ def get_pane(window: "Window", current_pane: t.Optional["Pane"] = None) -> "Pane else: pane = window.active_pane except exc.TmuxpException as e: - print(e) + print(e) # NOQA: T201 RUF100 if pane is None: if current_pane: diff --git a/src/tmuxp/workspace/builder.py b/src/tmuxp/workspace/builder.py index 3bcb78b8b83..e51ac7615d8 100644 --- a/src/tmuxp/workspace/builder.py +++ b/src/tmuxp/workspace/builder.py @@ -1,11 +1,12 @@ """Create a tmux workspace from a workspace :py:obj:`dict`.""" +from __future__ import annotations + import logging import os import shutil import time import typing as t -from collections.abc import Iterator from libtmux._internal.query_list import ObjectDoesNotExist from libtmux.common import has_gte_version, has_lt_version @@ -17,6 +18,9 @@ from tmuxp import exc from tmuxp.util import get_current_pane, run_before_script +if t.TYPE_CHECKING: + from collections.abc import Iterator + logger = logging.getLogger(__name__) COLUMNS_FALLBACK = 80 @@ -144,15 +148,15 @@ class WorkspaceBuilder: a session inside tmux (when `$TMUX` is in the env variables). """ - server: "Server" - _session: t.Optional["Session"] + server: Server + _session: Session | None session_name: str def __init__( self, session_config: dict[str, t.Any], server: Server, - plugins: t.Optional[list[t.Any]] = None, + plugins: list[t.Any] | None = None, ) -> None: """Initialize workspace loading. @@ -219,7 +223,7 @@ def session_exists(self, session_name: str) -> bool: return False return True - def build(self, session: t.Optional[Session] = None, append: bool = False) -> None: + def build(self, session: Session | None = None, append: bool = False) -> None: """Build tmux workspace in session. Optionally accepts ``session`` to build with only session object. @@ -482,7 +486,7 @@ def iter_create_panes( def get_pane_start_directory( pane_config: dict[str, str], window_config: dict[str, str], - ) -> t.Optional[str]: + ) -> str | None: if "start_directory" in pane_config: return pane_config["start_directory"] if "start_directory" in window_config: @@ -492,7 +496,7 @@ def get_pane_start_directory( def get_pane_shell( pane_config: dict[str, str], window_config: dict[str, str], - ) -> t.Optional[str]: + ) -> str | None: if "shell" in pane_config: return pane_config["shell"] if "window_shell" in window_config: diff --git a/src/tmuxp/workspace/constants.py b/src/tmuxp/workspace/constants.py index 48c2d18ba4c..48ecc9c1f6c 100644 --- a/src/tmuxp/workspace/constants.py +++ b/src/tmuxp/workspace/constants.py @@ -1,3 +1,5 @@ """Constant variables for tmuxp workspace functionality.""" +from __future__ import annotations + VALID_WORKSPACE_DIR_FILE_EXTENSIONS = [".yaml", ".yml", ".json"] diff --git a/src/tmuxp/workspace/finders.py b/src/tmuxp/workspace/finders.py index 2c202bb0fef..c657d389dde 100644 --- a/src/tmuxp/workspace/finders.py +++ b/src/tmuxp/workspace/finders.py @@ -1,27 +1,31 @@ """Workspace (configuration file) finders for tmuxp.""" +from __future__ import annotations + import logging import os -import pathlib import typing as t from colorama import Fore from tmuxp.cli.utils import tmuxp_echo -from tmuxp.types import StrPath from tmuxp.workspace.constants import VALID_WORKSPACE_DIR_FILE_EXTENSIONS logger = logging.getLogger(__name__) if t.TYPE_CHECKING: + import pathlib + from typing_extensions import TypeAlias + from tmuxp.types import StrPath + ValidExtensions: TypeAlias = t.Literal[".yml", ".yaml", ".json"] def is_workspace_file( filename: str, - extensions: t.Union["ValidExtensions", list["ValidExtensions"], None] = None, + extensions: ValidExtensions | list[ValidExtensions] | None = None, ) -> bool: """ Return True if file has a valid workspace file type. @@ -44,8 +48,8 @@ def is_workspace_file( def in_dir( - workspace_dir: t.Union[pathlib.Path, str, None] = None, - extensions: t.Optional[list["ValidExtensions"]] = None, + workspace_dir: pathlib.Path | str | None = None, + extensions: list[ValidExtensions] | None = None, ) -> list[str]: """ Return a list of workspace_files in ``workspace_dir``. @@ -130,7 +134,7 @@ def get_workspace_dir() -> str: def find_workspace_file( workspace_file: StrPath, - workspace_dir: t.Optional[StrPath] = None, + workspace_dir: StrPath | None = None, ) -> str: """ Return the real config path or raise an exception. diff --git a/src/tmuxp/workspace/freezer.py b/src/tmuxp/workspace/freezer.py index fba5103a73b..648a7755f10 100644 --- a/src/tmuxp/workspace/freezer.py +++ b/src/tmuxp/workspace/freezer.py @@ -1,11 +1,12 @@ """Tmux session freezing functionality for tmuxp.""" -import typing as t +from __future__ import annotations -from libtmux.pane import Pane -from libtmux.session import Session +import typing as t if t.TYPE_CHECKING: + from libtmux.pane import Pane + from libtmux.session import Session from libtmux.window import Window @@ -81,14 +82,14 @@ def freeze(session: Session) -> dict[str, t.Any]: # If all panes have same path, set 'start_directory' instead # of using 'cd' shell commands. - def pane_has_same_path(window: "Window", 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(window=window, pane=pane) for pane in window.panes): window_config["start_directory"] = window.panes[0].pane_current_path for pane in window.panes: - pane_config: t.Union[str, dict[str, t.Any]] = {"shell_command": []} + pane_config: str | dict[str, t.Any] = {"shell_command": []} assert isinstance(pane_config, dict) if "start_directory" not in window_config and pane.pane_current_path: @@ -99,7 +100,7 @@ def pane_has_same_path(window: "Window", pane: Pane) -> bool: current_cmd = pane.pane_current_command - def filter_interpreters_and_shells(current_cmd: t.Optional[str]) -> bool: + def filter_interpreters_and_shells(current_cmd: str | None) -> bool: return current_cmd is not None and ( current_cmd.startswith("-") or any( diff --git a/src/tmuxp/workspace/importers.py b/src/tmuxp/workspace/importers.py index 2ee9c7880fd..7a4c189b70c 100644 --- a/src/tmuxp/workspace/importers.py +++ b/src/tmuxp/workspace/importers.py @@ -1,5 +1,7 @@ """Configuration import adapters to load teamocil, tmuxinator, etc. in tmuxp.""" +from __future__ import annotations + import typing as t diff --git a/src/tmuxp/workspace/loader.py b/src/tmuxp/workspace/loader.py index bf9c7c098ad..613b2b589df 100644 --- a/src/tmuxp/workspace/loader.py +++ b/src/tmuxp/workspace/loader.py @@ -1,5 +1,7 @@ """Workspace hydration and loading for tmuxp.""" +from __future__ import annotations + import logging import os import pathlib @@ -65,8 +67,8 @@ def expand_cmd(p: dict[str, t.Any]) -> dict[str, t.Any]: def expand( workspace_dict: dict[str, t.Any], - cwd: t.Optional[t.Union[pathlib.Path, str]] = None, - parent: t.Optional[t.Any] = None, + cwd: pathlib.Path | str | None = None, + parent: t.Any | None = None, ) -> dict[str, t.Any]: """Resolve workspace variables and expand shorthand style / inline properties. diff --git a/src/tmuxp/workspace/validation.py b/src/tmuxp/workspace/validation.py index b200bd63080..fd549856ec5 100644 --- a/src/tmuxp/workspace/validation.py +++ b/src/tmuxp/workspace/validation.py @@ -1,5 +1,7 @@ """Validation errors for tmuxp configuration files.""" +from __future__ import annotations + import typing as t from tmuxp import exc diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 2c271d39e29..14e0b30da0c 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1,5 +1,7 @@ """CLI tests for tmuxp's core shell functionality.""" +from __future__ import annotations + import argparse import contextlib import pathlib @@ -112,7 +114,7 @@ def test_pass_config_dir_argparse( def config_cmd(workspace_file: str) -> None: tmuxp_echo(find_workspace_file(workspace_file, workspace_dir=configdir)) - def check_cmd(config_arg: str) -> "_pytest.capture.CaptureResult[str]": + def check_cmd(config_arg: str) -> _pytest.capture.CaptureResult[str]: args = parser.parse_args([config_arg]) config_cmd(workspace_file=args.workspace_file) return capsys.readouterr() @@ -130,7 +132,7 @@ def check_cmd(config_arg: str) -> "_pytest.capture.CaptureResult[str]": def test_reattach_plugins( monkeypatch_plugin_test_packages: None, - server: "Server", + server: Server, ) -> None: """Test reattach plugin hook.""" config_plugins = test_utils.read_workspace_file("workspace/builder/plugin_r.yaml") diff --git a/tests/cli/test_convert.py b/tests/cli/test_convert.py index d9de79ede82..f57f771f778 100644 --- a/tests/cli/test_convert.py +++ b/tests/cli/test_convert.py @@ -1,14 +1,19 @@ """CLI tests for tmuxp convert.""" +from __future__ import annotations + import contextlib import io import json -import pathlib +import typing as t import pytest from tmuxp import cli +if t.TYPE_CHECKING: + import pathlib + @pytest.mark.parametrize( "cli_args", diff --git a/tests/cli/test_debug_info.py b/tests/cli/test_debug_info.py index 8bebb3e9cf8..1729f1e9cc8 100644 --- a/tests/cli/test_debug_info.py +++ b/tests/cli/test_debug_info.py @@ -1,11 +1,16 @@ """CLI tests for tmuxp debuginfo.""" -import pathlib +from __future__ import annotations -import pytest +import typing as t from tmuxp import cli +if t.TYPE_CHECKING: + import pathlib + + import pytest + def test_debug_info_cli( monkeypatch: pytest.MonkeyPatch, diff --git a/tests/cli/test_freeze.py b/tests/cli/test_freeze.py index 0d2d184ba69..ef3a15812ea 100644 --- a/tests/cli/test_freeze.py +++ b/tests/cli/test_freeze.py @@ -1,8 +1,9 @@ """Test workspace freezing functionality for tmuxp.""" +from __future__ import annotations + import contextlib import io -import pathlib import typing as t import pytest @@ -11,6 +12,8 @@ from tmuxp._internal.config_reader import ConfigReader if t.TYPE_CHECKING: + import pathlib + from libtmux.server import Server @@ -30,7 +33,7 @@ ], ) def test_freeze( - server: "Server", + server: Server, cli_args: list[str], inputs: list[str], tmp_path: pathlib.Path, @@ -82,7 +85,7 @@ def test_freeze( ], ) def test_freeze_overwrite( - server: "Server", + server: Server, cli_args: list[str], inputs: list[str], tmp_path: pathlib.Path, diff --git a/tests/cli/test_import.py b/tests/cli/test_import.py index 2e1e1497f96..a3bfe71a7f1 100644 --- a/tests/cli/test_import.py +++ b/tests/cli/test_import.py @@ -1,14 +1,19 @@ """CLI tests for tmuxp import.""" +from __future__ import annotations + import contextlib import io -import pathlib +import typing as t import pytest from tests.fixtures import utils as test_utils from tmuxp import cli +if t.TYPE_CHECKING: + import pathlib + @pytest.mark.parametrize("cli_args", [(["import"])]) def test_import( diff --git a/tests/cli/test_load.py b/tests/cli/test_load.py index fd04f487626..95925a1dd99 100644 --- a/tests/cli/test_load.py +++ b/tests/cli/test_load.py @@ -1,5 +1,7 @@ """CLI tests for tmuxp load.""" +from __future__ import annotations + import contextlib import io import pathlib @@ -10,7 +12,6 @@ from libtmux.common import has_lt_version from libtmux.server import Server from libtmux.session import Session -from pytest_mock import MockerFixture from tests.constants import FIXTURE_PATH from tests.fixtures import utils as test_utils @@ -27,7 +28,7 @@ def test_load_workspace( - server: "Server", + server: Server, monkeypatch: pytest.MonkeyPatch, ) -> None: """Generic test for loading a tmuxp workspace via tmuxp load.""" @@ -49,7 +50,7 @@ def test_load_workspace( def test_load_workspace_passes_tmux_config( - server: "Server", + server: Server, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test tmuxp load with a tmux configuration file.""" @@ -73,7 +74,7 @@ def test_load_workspace_passes_tmux_config( def test_load_workspace_named_session( - server: "Server", + server: Server, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test tmuxp load with a custom tmux session name.""" @@ -101,7 +102,7 @@ def test_load_workspace_named_session( ) def test_load_workspace_name_match_regression_252( tmp_path: pathlib.Path, - server: "Server", + server: Server, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test tmuxp load for a regression where tmux shell names would not match.""" @@ -141,7 +142,7 @@ def test_load_workspace_name_match_regression_252( def test_load_symlinked_workspace( - server: "Server", + server: Server, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -185,6 +186,7 @@ def test_load_symlinked_workspace( if t.TYPE_CHECKING: + from pytest_mock import MockerFixture from typing_extensions import TypeAlias ExpectedOutput: TypeAlias = t.Optional[t.Union[str, list[str]]] @@ -197,14 +199,14 @@ class CLILoadFixture(t.NamedTuple): test_id: str # test params - cli_args: list[t.Union[str, list[str]]] + cli_args: list[str | list[str]] config_paths: list[str] session_names: list[str] expected_exit_code: int - expected_in_out: "ExpectedOutput" = None - expected_not_in_out: "ExpectedOutput" = None - expected_in_err: "ExpectedOutput" = None - expected_not_in_err: "ExpectedOutput" = None + expected_in_out: ExpectedOutput = None + expected_not_in_out: ExpectedOutput = None + expected_in_err: ExpectedOutput = None + expected_not_in_err: ExpectedOutput = None TEST_LOAD_FIXTURES: list[CLILoadFixture] = [ @@ -289,7 +291,7 @@ class CLILoadFixture(t.NamedTuple): def test_load( tmp_path: pathlib.Path, tmuxp_configdir: pathlib.Path, - server: "Server", + server: Server, session: Session, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, @@ -298,10 +300,10 @@ def test_load( config_paths: list[str], session_names: list[str], expected_exit_code: int, - expected_in_out: "ExpectedOutput", - expected_not_in_out: "ExpectedOutput", - expected_in_err: "ExpectedOutput", - expected_not_in_err: "ExpectedOutput", + expected_in_out: ExpectedOutput, + expected_not_in_out: ExpectedOutput, + expected_in_err: ExpectedOutput, + expected_not_in_err: ExpectedOutput, ) -> None: """Parametrized test battery for tmuxp load CLI command.""" assert server.socket_name is not None @@ -346,7 +348,7 @@ def test_load( def test_regression_00132_session_name_with_dots( tmp_path: pathlib.Path, - server: "Server", + server: Server, session: Session, capsys: pytest.CaptureFixture[str], ) -> None: @@ -366,7 +368,7 @@ def test_load_zsh_autotitle_warning( tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], - server: "Server", + server: Server, ) -> None: """Test loading ZSH without DISABLE_AUTO_TITLE raises warning.""" # create dummy tmuxp yaml so we don't get yelled at @@ -548,7 +550,7 @@ def test_load_plugins_plugin_missing( def test_plugin_system_before_script( monkeypatch_plugin_test_packages: None, - server: "Server", + server: Server, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test tmuxp load with sessions using before_script.""" @@ -570,7 +572,7 @@ def test_plugin_system_before_script( def test_load_attached( - server: "Server", + server: Server, monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, ) -> None: @@ -592,7 +594,7 @@ def test_load_attached( def test_load_attached_detached( - server: "Server", + server: Server, monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, ) -> None: @@ -614,7 +616,7 @@ def test_load_attached_detached( def test_load_attached_within_tmux( - server: "Server", + server: Server, monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, ) -> None: @@ -636,7 +638,7 @@ def test_load_attached_within_tmux( def test_load_attached_within_tmux_detached( - server: "Server", + server: Server, monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, ) -> None: @@ -658,7 +660,7 @@ def test_load_attached_within_tmux_detached( def test_load_append_windows_to_current_session( - server: "Server", + server: Server, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test tmuxp load when windows are appended to the current session.""" diff --git a/tests/cli/test_ls.py b/tests/cli/test_ls.py index 9532dd3f7cd..1811e636c42 100644 --- a/tests/cli/test_ls.py +++ b/tests/cli/test_ls.py @@ -1,12 +1,16 @@ """CLI tests for tmuxp ls command.""" +from __future__ import annotations + import contextlib import pathlib - -import pytest +import typing as t from tmuxp import cli +if t.TYPE_CHECKING: + import pytest + def test_ls_cli( monkeypatch: pytest.MonkeyPatch, diff --git a/tests/cli/test_shell.py b/tests/cli/test_shell.py index 6690b2a3332..65ad67b2efb 100644 --- a/tests/cli/test_shell.py +++ b/tests/cli/test_shell.py @@ -1,18 +1,21 @@ """CLI tests for tmuxp shell.""" +from __future__ import annotations + import contextlib import io -import pathlib import subprocess import typing as t import pytest -from libtmux.session import Session from tmuxp import cli, exc if t.TYPE_CHECKING: + import pathlib + from libtmux.server import Server + from libtmux.session import Session class CLIShellFixture(t.NamedTuple): @@ -114,7 +117,7 @@ def test_shell( inputs: list[t.Any], env: dict[str, str], expected_output: str, - server: "Server", + server: Server, session: Session, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, @@ -201,13 +204,10 @@ def test_shell_target_missing( inputs: list[t.Any], env: dict[t.Any, t.Any], template_ctx: dict[str, str], - exception: t.Union[ - type[exc.TmuxpException], - type[subprocess.CalledProcessError], - ], + exception: type[exc.TmuxpException | subprocess.CalledProcessError], message: str, socket_name: str, - server: "Server", + server: Server, session: Session, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, @@ -284,7 +284,7 @@ def test_shell_interactive( inputs: list[t.Any], env: dict[str, str], message: str, - server: "Server", + server: Server, session: Session, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, diff --git a/tests/constants.py b/tests/constants.py index ff3cc8bd0f4..ee67d4c7708 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -1,5 +1,7 @@ """Constant variables for tmuxp tests.""" +from __future__ import annotations + import pathlib TESTS_PATH = pathlib.Path(__file__).parent diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py index fd26c7acab8..2f0841acd81 100644 --- a/tests/fixtures/__init__.py +++ b/tests/fixtures/__init__.py @@ -1,3 +1,5 @@ """Fixture test data for tmuxp.""" +from __future__ import annotations + from . import utils diff --git a/tests/fixtures/import_teamocil/__init__.py b/tests/fixtures/import_teamocil/__init__.py index cb19a6253be..1ec7c59fd55 100644 --- a/tests/fixtures/import_teamocil/__init__.py +++ b/tests/fixtures/import_teamocil/__init__.py @@ -1,3 +1,5 @@ """Teamocil data fixtures for import_teamocil tests.""" +from __future__ import annotations + from . import layouts, test1, test2, test3, test4 diff --git a/tests/fixtures/import_teamocil/layouts.py b/tests/fixtures/import_teamocil/layouts.py index d09e0fbf0a1..6e3e06bf7f9 100644 --- a/tests/fixtures/import_teamocil/layouts.py +++ b/tests/fixtures/import_teamocil/layouts.py @@ -1,5 +1,7 @@ """Teamocil data fixtures for import_teamocil tests, for layout testing.""" +from __future__ import annotations + from tests.fixtures import utils as test_utils teamocil_yaml_file = test_utils.get_workspace_file("import_teamocil/layouts.yaml") diff --git a/tests/fixtures/import_teamocil/test1.py b/tests/fixtures/import_teamocil/test1.py index 38246268760..8e2065fec21 100644 --- a/tests/fixtures/import_teamocil/test1.py +++ b/tests/fixtures/import_teamocil/test1.py @@ -1,5 +1,7 @@ """Teamocil data fixtures for import_teamocil tests, 1st test.""" +from __future__ import annotations + from tests.fixtures import utils as test_utils teamocil_yaml = test_utils.read_workspace_file("import_teamocil/test1.yaml") diff --git a/tests/fixtures/import_teamocil/test2.py b/tests/fixtures/import_teamocil/test2.py index ad6986880f0..0353a0edbf5 100644 --- a/tests/fixtures/import_teamocil/test2.py +++ b/tests/fixtures/import_teamocil/test2.py @@ -1,5 +1,7 @@ """Teamocil data fixtures for import_teamocil tests, 2nd test.""" +from __future__ import annotations + from tests.fixtures import utils as test_utils teamocil_yaml = test_utils.read_workspace_file("import_teamocil/test2.yaml") diff --git a/tests/fixtures/import_teamocil/test3.py b/tests/fixtures/import_teamocil/test3.py index 5b2687c1c49..1bcd32fef13 100644 --- a/tests/fixtures/import_teamocil/test3.py +++ b/tests/fixtures/import_teamocil/test3.py @@ -1,5 +1,7 @@ """Teamocil data fixtures for import_teamocil tests, 3rd test.""" +from __future__ import annotations + from tests.fixtures import utils as test_utils teamocil_yaml = test_utils.read_workspace_file("import_teamocil/test3.yaml") diff --git a/tests/fixtures/import_teamocil/test4.py b/tests/fixtures/import_teamocil/test4.py index b7883651b38..1837abf5086 100644 --- a/tests/fixtures/import_teamocil/test4.py +++ b/tests/fixtures/import_teamocil/test4.py @@ -1,5 +1,7 @@ """Teamocil data fixtures for import_teamocil tests, 4th test.""" +from __future__ import annotations + from tests.fixtures import utils as test_utils teamocil_yaml = test_utils.read_workspace_file("import_teamocil/test4.yaml") diff --git a/tests/fixtures/import_tmuxinator/__init__.py b/tests/fixtures/import_tmuxinator/__init__.py index 1ff1cafde3c..84508e04051 100644 --- a/tests/fixtures/import_tmuxinator/__init__.py +++ b/tests/fixtures/import_tmuxinator/__init__.py @@ -1,3 +1,5 @@ """Tmuxinator data fixtures for import_tmuxinator tests.""" +from __future__ import annotations + from . import test1, test2, test3 diff --git a/tests/fixtures/import_tmuxinator/test1.py b/tests/fixtures/import_tmuxinator/test1.py index 03fbc4196fd..7e08f976d0e 100644 --- a/tests/fixtures/import_tmuxinator/test1.py +++ b/tests/fixtures/import_tmuxinator/test1.py @@ -1,5 +1,7 @@ """Tmuxinator data fixtures for import_tmuxinator tests, 1st dataset.""" +from __future__ import annotations + from tests.fixtures import utils as test_utils tmuxinator_yaml = test_utils.read_workspace_file("import_tmuxinator/test1.yaml") diff --git a/tests/fixtures/import_tmuxinator/test2.py b/tests/fixtures/import_tmuxinator/test2.py index 12fef7754af..97d923a912d 100644 --- a/tests/fixtures/import_tmuxinator/test2.py +++ b/tests/fixtures/import_tmuxinator/test2.py @@ -1,5 +1,7 @@ """Tmuxinator data fixtures for import_tmuxinator tests, 2nd dataset.""" +from __future__ import annotations + from tests.fixtures import utils as test_utils tmuxinator_yaml = test_utils.read_workspace_file("import_tmuxinator/test2.yaml") diff --git a/tests/fixtures/import_tmuxinator/test3.py b/tests/fixtures/import_tmuxinator/test3.py index ed479c57b57..86ebd22c16e 100644 --- a/tests/fixtures/import_tmuxinator/test3.py +++ b/tests/fixtures/import_tmuxinator/test3.py @@ -1,5 +1,7 @@ """Tmuxinator data fixtures for import_tmuxinator tests, 3rd dataset.""" +from __future__ import annotations + from tests.fixtures import utils as test_utils tmuxinator_yaml = test_utils.read_workspace_file("import_tmuxinator/test3.yaml") diff --git a/tests/fixtures/pluginsystem/partials/_types.py b/tests/fixtures/pluginsystem/partials/_types.py index 77d2328cd53..0809596bdf7 100644 --- a/tests/fixtures/pluginsystem/partials/_types.py +++ b/tests/fixtures/pluginsystem/partials/_types.py @@ -10,6 +10,8 @@ ... """ +from __future__ import annotations + from typing_extensions import NotRequired, TypedDict diff --git a/tests/fixtures/pluginsystem/partials/all_pass.py b/tests/fixtures/pluginsystem/partials/all_pass.py index 357cfff4948..293d70bef1b 100644 --- a/tests/fixtures/pluginsystem/partials/all_pass.py +++ b/tests/fixtures/pluginsystem/partials/all_pass.py @@ -1,5 +1,7 @@ """Tmuxp test plugin with version constraints guaranteed to pass.""" +from __future__ import annotations + import typing as t from .test_plugin_helpers import MyTestTmuxpPlugin diff --git a/tests/fixtures/pluginsystem/partials/libtmux_version_fail.py b/tests/fixtures/pluginsystem/partials/libtmux_version_fail.py index 85a1e511843..e69efaa3f09 100644 --- a/tests/fixtures/pluginsystem/partials/libtmux_version_fail.py +++ b/tests/fixtures/pluginsystem/partials/libtmux_version_fail.py @@ -1,5 +1,7 @@ """Fixtures for tmuxp plugins for libtmux version exceptions.""" +from __future__ import annotations + import typing as t from .test_plugin_helpers import MyTestTmuxpPlugin diff --git a/tests/fixtures/pluginsystem/partials/test_plugin_helpers.py b/tests/fixtures/pluginsystem/partials/test_plugin_helpers.py index 5c27dce976e..d73674cf3f7 100644 --- a/tests/fixtures/pluginsystem/partials/test_plugin_helpers.py +++ b/tests/fixtures/pluginsystem/partials/test_plugin_helpers.py @@ -1,5 +1,7 @@ """Tmuxp test plugin for asserting version constraints.""" +from __future__ import annotations + import typing as t from tmuxp.plugin import TmuxpPlugin @@ -15,7 +17,7 @@ class MyTestTmuxpPlugin(TmuxpPlugin): """Base class for testing tmuxp plugins with version constraints.""" - def __init__(self, **config: "Unpack[PluginTestConfigSchema]") -> None: + def __init__(self, **config: Unpack[PluginTestConfigSchema]) -> None: assert isinstance(config, dict) tmux_version = config.pop("tmux_version", None) libtmux_version = config.pop("libtmux_version", None) diff --git a/tests/fixtures/pluginsystem/partials/tmux_version_fail.py b/tests/fixtures/pluginsystem/partials/tmux_version_fail.py index 6a91f6d960d..deaa89d2cf6 100644 --- a/tests/fixtures/pluginsystem/partials/tmux_version_fail.py +++ b/tests/fixtures/pluginsystem/partials/tmux_version_fail.py @@ -1,5 +1,7 @@ """Fixtures for tmuxp plugins for tmux version exceptions.""" +from __future__ import annotations + import typing as t from .test_plugin_helpers import MyTestTmuxpPlugin diff --git a/tests/fixtures/pluginsystem/partials/tmuxp_version_fail.py b/tests/fixtures/pluginsystem/partials/tmuxp_version_fail.py index 703e7b3d258..aa5b8e77834 100644 --- a/tests/fixtures/pluginsystem/partials/tmuxp_version_fail.py +++ b/tests/fixtures/pluginsystem/partials/tmuxp_version_fail.py @@ -1,5 +1,7 @@ """Fixtures for tmuxp plugins for tmuxp version exceptions.""" +from __future__ import annotations + import typing as t from .test_plugin_helpers import MyTestTmuxpPlugin diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_awf/tmuxp_test_plugin_awf/plugin.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_awf/tmuxp_test_plugin_awf/plugin.py index edab6ed6868..fe4a604ab1b 100644 --- a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_awf/tmuxp_test_plugin_awf/plugin.py +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_awf/tmuxp_test_plugin_awf/plugin.py @@ -1,5 +1,7 @@ """Tmuxp example plugin for after_window_finished.""" +from __future__ import annotations + import typing as t from tmuxp.plugin import TmuxpPlugin @@ -14,7 +16,7 @@ class PluginAfterWindowFinished(TmuxpPlugin): def __init__(self) -> None: self.message: str = "[+] This is the Tmuxp Test Plugin" - def after_window_finished(self, window: "Window") -> None: + def after_window_finished(self, window: Window) -> None: """Run hook after window creation completed.""" if window.name == "editor": window.rename_window("plugin_test_awf") diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bs/tmuxp_test_plugin_bs/plugin.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bs/tmuxp_test_plugin_bs/plugin.py index 6da3e3ae2b4..9bc64c9e37f 100644 --- a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bs/tmuxp_test_plugin_bs/plugin.py +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bs/tmuxp_test_plugin_bs/plugin.py @@ -1,5 +1,7 @@ """Tmux plugin that runs before_script, if it is declared in configuration.""" +from __future__ import annotations + import typing as t from tmuxp.plugin import TmuxpPlugin @@ -14,6 +16,6 @@ class PluginBeforeScript(TmuxpPlugin): def __init__(self) -> None: self.message: str = "[+] This is the Tmuxp Test Plugin" - def before_script(self, session: "Session") -> None: + def before_script(self, session: Session) -> None: """Run hook during before_script, if it is declared.""" session.rename_session("plugin_test_bs") diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bwb/tmuxp_test_plugin_bwb/plugin.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bwb/tmuxp_test_plugin_bwb/plugin.py index fba2134e15d..b564ec58298 100644 --- a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bwb/tmuxp_test_plugin_bwb/plugin.py +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_bwb/tmuxp_test_plugin_bwb/plugin.py @@ -1,5 +1,7 @@ """Tmuxp example plugin for before_worksplace_builder.""" +from __future__ import annotations + import typing as t from tmuxp.plugin import TmuxpPlugin @@ -14,6 +16,6 @@ class PluginBeforeWorkspaceBuilder(TmuxpPlugin): def __init__(self) -> None: self.message: str = "[+] This is the Tmuxp Test Plugin" - def before_workspace_builder(self, session: "Session") -> None: + def before_workspace_builder(self, session: Session) -> None: """Run hook before workspace builder begins.""" session.rename_session("plugin_test_bwb") diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/tmuxp_test_plugin_fail/plugin.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/tmuxp_test_plugin_fail/plugin.py index 520cf55c93c..9be75a6e93e 100644 --- a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/tmuxp_test_plugin_fail/plugin.py +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_fail/tmuxp_test_plugin_fail/plugin.py @@ -1,5 +1,7 @@ """Tmuxp example plugin that fails on initialization.""" +from __future__ import annotations + import typing as t from tmuxp.plugin import TmuxpPlugin diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_owc/tmuxp_test_plugin_owc/plugin.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_owc/tmuxp_test_plugin_owc/plugin.py index 8c330a648c3..d21006ce997 100644 --- a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_owc/tmuxp_test_plugin_owc/plugin.py +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_owc/tmuxp_test_plugin_owc/plugin.py @@ -1,5 +1,7 @@ """Tmuxp example plugin for on_window_create.""" +from __future__ import annotations + import typing as t from tmuxp.plugin import TmuxpPlugin @@ -14,7 +16,7 @@ class PluginOnWindowCreate(TmuxpPlugin): def __init__(self) -> None: self.message: str = "[+] This is the Tmuxp Test Plugin" - def on_window_create(self, window: "Window") -> None: + def on_window_create(self, window: Window) -> None: """Apply hook that runs for tmux on session reattach.""" if window.name == "editor": window.rename_window("plugin_test_owc") diff --git a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_r/tmuxp_test_plugin_r/plugin.py b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_r/tmuxp_test_plugin_r/plugin.py index 373e9e9580f..396afabf5a0 100644 --- a/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_r/tmuxp_test_plugin_r/plugin.py +++ b/tests/fixtures/pluginsystem/plugins/tmuxp_test_plugin_r/tmuxp_test_plugin_r/plugin.py @@ -1,5 +1,7 @@ """Tmuxp example plugin for reattaching session.""" +from __future__ import annotations + import typing as t from tmuxp.plugin import TmuxpPlugin @@ -14,6 +16,6 @@ class PluginReattach(TmuxpPlugin): def __init__(self) -> None: self.message: str = "[+] This is the Tmuxp Test Plugin" - def reattach(self, session: "Session") -> None: + def reattach(self, session: Session) -> None: """Apply hook that runs for tmux on session reattach.""" session.rename_session("plugin_test_r") diff --git a/tests/fixtures/structures.py b/tests/fixtures/structures.py index 4e6ca17c03f..5c7587e4ce1 100644 --- a/tests/fixtures/structures.py +++ b/tests/fixtures/structures.py @@ -1,5 +1,7 @@ """Typings / structures for tmuxp fixtures.""" +from __future__ import annotations + import dataclasses import typing as t diff --git a/tests/fixtures/utils.py b/tests/fixtures/utils.py index 81fd07cf897..23c4ac26fd4 100644 --- a/tests/fixtures/utils.py +++ b/tests/fixtures/utils.py @@ -1,13 +1,14 @@ """Utility functions for tmuxp fixtures.""" +from __future__ import annotations + import pathlib -import typing as t from tests.constants import FIXTURE_PATH def get_workspace_file( - file: t.Union[str, pathlib.Path], + file: str | pathlib.Path, ) -> pathlib.Path: """Return fixture data, relative to __file__.""" if isinstance(file, str): @@ -17,7 +18,7 @@ def get_workspace_file( def read_workspace_file( - file: t.Union[pathlib.Path, str], + file: pathlib.Path | str, ) -> str: """Return fixture data, relative to __file__.""" if isinstance(file, str): diff --git a/tests/fixtures/workspace/__init__.py b/tests/fixtures/workspace/__init__.py index e80a60fabdf..b097ef9cb65 100644 --- a/tests/fixtures/workspace/__init__.py +++ b/tests/fixtures/workspace/__init__.py @@ -1,5 +1,7 @@ """Workspace data fixtures for tmuxp tests.""" +from __future__ import annotations + from . import ( expand1, expand2, diff --git a/tests/fixtures/workspace/expand1.py b/tests/fixtures/workspace/expand1.py index 75d5d06c009..6ad0cbb9e39 100644 --- a/tests/fixtures/workspace/expand1.py +++ b/tests/fixtures/workspace/expand1.py @@ -1,5 +1,7 @@ """Examples of expansion of tmuxp configurations from shorthand style.""" +from __future__ import annotations + import pathlib import typing as t diff --git a/tests/fixtures/workspace/expand2.py b/tests/fixtures/workspace/expand2.py index 3acdc3f3874..7e8089bbbc6 100644 --- a/tests/fixtures/workspace/expand2.py +++ b/tests/fixtures/workspace/expand2.py @@ -1,5 +1,7 @@ """YAML examples of expansion of tmuxp configurations from shorthand style.""" +from __future__ import annotations + import pathlib from tests.fixtures import utils as test_utils diff --git a/tests/fixtures/workspace/expand_blank.py b/tests/fixtures/workspace/expand_blank.py index a9bbc70e516..51376052976 100644 --- a/tests/fixtures/workspace/expand_blank.py +++ b/tests/fixtures/workspace/expand_blank.py @@ -1,5 +1,7 @@ """Expected expanded configuration for empty workspace panes.""" +from __future__ import annotations + expected = { "session_name": "Blank pane test", "windows": [ diff --git a/tests/fixtures/workspace/sample_workspace.py b/tests/fixtures/workspace/sample_workspace.py index e3bf60347bf..e00b15113af 100644 --- a/tests/fixtures/workspace/sample_workspace.py +++ b/tests/fixtures/workspace/sample_workspace.py @@ -1,5 +1,7 @@ """Example workspace fixture for tmuxp WorkspaceBuilder.""" +from __future__ import annotations + sample_workspace_dict = { "session_name": "sample workspace", "start_directory": "~", diff --git a/tests/fixtures/workspace/shell_command_before.py b/tests/fixtures/workspace/shell_command_before.py index 10b74518ddc..53f8e1587f8 100644 --- a/tests/fixtures/workspace/shell_command_before.py +++ b/tests/fixtures/workspace/shell_command_before.py @@ -1,5 +1,7 @@ """Test fixture for tmuxp to demonstrate shell_command_before.""" +from __future__ import annotations + import pathlib import typing as t diff --git a/tests/fixtures/workspace/shell_command_before_session.py b/tests/fixtures/workspace/shell_command_before_session.py index f3639ded0ce..adfc86f1838 100644 --- a/tests/fixtures/workspace/shell_command_before_session.py +++ b/tests/fixtures/workspace/shell_command_before_session.py @@ -1,5 +1,7 @@ """Tests shell_command_before configuration.""" +from __future__ import annotations + from tests.fixtures import utils as test_utils before = test_utils.read_workspace_file("workspace/shell_command_before_session.yaml") diff --git a/tests/fixtures/workspace/trickle.py b/tests/fixtures/workspace/trickle.py index afb138081ea..a1012be6473 100644 --- a/tests/fixtures/workspace/trickle.py +++ b/tests/fixtures/workspace/trickle.py @@ -1,5 +1,7 @@ """Test data for tmuxp workspace fixture to demo object tree inheritance.""" +from __future__ import annotations + before = { # shell_command_before is string in some areas "session_name": "sample workspace", "start_directory": "/var", diff --git a/tests/test_plugin.py b/tests/test_plugin.py index feee07d3755..cf7cfc63718 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1,5 +1,7 @@ """Tests for tmuxp plugin API.""" +from __future__ import annotations + import pytest from tmuxp.exc import TmuxpPluginException diff --git a/tests/test_shell.py b/tests/test_shell.py index c8cf67b8f94..d544439d606 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -1,5 +1,7 @@ """Tests for tmuxp shell module.""" +from __future__ import annotations + from tmuxp import shell diff --git a/tests/test_util.py b/tests/test_util.py index 04e29406887..5b58b493d5b 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,7 +1,10 @@ """Tests for tmuxp's utility functions.""" +from __future__ import annotations + +import typing as t + import pytest -from libtmux.server import Server from tmuxp import exc from tmuxp.exc import BeforeLoadScriptError, BeforeLoadScriptNotExists @@ -9,6 +12,9 @@ from .constants import FIXTURE_PATH +if t.TYPE_CHECKING: + from libtmux.server import Server + def test_run_before_script_raise_BeforeLoadScriptNotExists_if_not_exists() -> None: """run_before_script() raises BeforeLoadScriptNotExists if script not found.""" diff --git a/tests/tests/test_helpers.py b/tests/tests/test_helpers.py index 1b9658ba816..0888cd5e031 100644 --- a/tests/tests/test_helpers.py +++ b/tests/tests/test_helpers.py @@ -1,9 +1,15 @@ """Tests for tmuxp's helper and utility functions.""" +from __future__ import annotations + +import typing as t + import pytest -from libtmux.server import Server from libtmux.test import get_test_session_name, temp_session +if t.TYPE_CHECKING: + from libtmux.server import Server + def test_temp_session_kills_session_on_exit(server: Server) -> None: """Test temp_session() context manager kills session on exit.""" diff --git a/tests/workspace/conftest.py b/tests/workspace/conftest.py index b3549b8c13b..403189710a3 100644 --- a/tests/workspace/conftest.py +++ b/tests/workspace/conftest.py @@ -1,5 +1,7 @@ """Pytest configuration for tmuxp workspace tests.""" +from __future__ import annotations + import types import pytest diff --git a/tests/workspace/test_builder.py b/tests/workspace/test_builder.py index c5d2b608ae1..8c5f00e2d26 100644 --- a/tests/workspace/test_builder.py +++ b/tests/workspace/test_builder.py @@ -1,5 +1,7 @@ """Test for tmuxp workspace builder.""" +from __future__ import annotations + import functools import os import pathlib @@ -13,7 +15,6 @@ from libtmux.common import has_gte_version, has_lt_version from libtmux.exc import LibTmuxException from libtmux.pane import Pane -from libtmux.server import Server from libtmux.session import Session from libtmux.test import retry_until, temp_session from libtmux.window import Window @@ -27,6 +28,7 @@ from tmuxp.workspace.builder import WorkspaceBuilder if t.TYPE_CHECKING: + from libtmux.server import Server class AssertCallbackProtocol(t.Protocol): """Assertion callback type protocol.""" @@ -481,7 +483,7 @@ def test_environment_variables_warns_prior_to_tmux_3_0( def test_automatic_rename_option( - server: "Server", + server: Server, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test workspace builder with automatic renaming enabled.""" @@ -1166,7 +1168,7 @@ def test_find_current_active_pane( "___4___", False, ), - ( + ( # NOQA: PT014 RUF100 textwrap.dedent( """ session_name: Should not execute @@ -1180,7 +1182,7 @@ def test_find_current_active_pane( "___4___", False, ), - ( + ( # NOQA: PT014 RUF100 textwrap.dedent( """ session_name: Should execute @@ -1461,7 +1463,7 @@ class DefaultSizeNamespaceFixture(t.NamedTuple): test_id: str # test params - TMUXP_DEFAULT_SIZE: t.Optional[str] + TMUXP_DEFAULT_SIZE: str | None raises: bool confoverrides: dict[str, t.Any] @@ -1495,10 +1497,10 @@ class DefaultSizeNamespaceFixture(t.NamedTuple): ) @pytest.mark.skipif(has_lt_version("2.9"), reason="default-size only applies there") def test_issue_800_default_size_many_windows( - server: "Server", + server: Server, monkeypatch: pytest.MonkeyPatch, test_id: str, - TMUXP_DEFAULT_SIZE: t.Optional[str], + TMUXP_DEFAULT_SIZE: str | None, raises: bool, confoverrides: dict[str, t.Any], ) -> None: diff --git a/tests/workspace/test_config.py b/tests/workspace/test_config.py index f04cd1d93b1..02ebcf5ffa2 100644 --- a/tests/workspace/test_config.py +++ b/tests/workspace/test_config.py @@ -1,5 +1,7 @@ """Test for tmuxp configuration import, inlining, expanding and export.""" +from __future__ import annotations + import pathlib import typing as t @@ -14,7 +16,7 @@ from tests.fixtures.structures import WorkspaceTestData -def load_workspace(path: t.Union[str, pathlib.Path]) -> dict[str, t.Any]: +def load_workspace(path: str | pathlib.Path) -> dict[str, t.Any]: """Load tmuxp workspace configuration from file.""" return ConfigReader._from_file( pathlib.Path(path) if isinstance(path, str) else path, @@ -23,7 +25,7 @@ def load_workspace(path: t.Union[str, pathlib.Path]) -> dict[str, t.Any]: def test_export_json( tmp_path: pathlib.Path, - config_fixture: "WorkspaceTestData", + config_fixture: WorkspaceTestData, ) -> None: """Test exporting configuration dictionary to JSON.""" json_workspace_file = tmp_path / "config.json" @@ -38,13 +40,13 @@ def test_export_json( assert config_fixture.sample_workspace.sample_workspace_dict == new_workspace_data -def test_workspace_expand1(config_fixture: "WorkspaceTestData") -> None: +def test_workspace_expand1(config_fixture: WorkspaceTestData) -> None: """Expand shell commands from string to list.""" test_workspace = loader.expand(config_fixture.expand1.before_workspace) assert test_workspace == config_fixture.expand1.after_workspace() -def test_workspace_expand2(config_fixture: "WorkspaceTestData") -> None: +def test_workspace_expand2(config_fixture: WorkspaceTestData) -> None: """Expand shell commands from string to list.""" unexpanded_dict = ConfigReader._load( fmt="yaml", @@ -127,7 +129,7 @@ def test_inheritance_workspace() -> None: assert workspace == inheritance_workspace_after -def test_shell_command_before(config_fixture: "WorkspaceTestData") -> None: +def test_shell_command_before(config_fixture: WorkspaceTestData) -> None: """Config inheritance for the nested 'start_command'.""" test_workspace = config_fixture.shell_command_before.config_unexpanded test_workspace = loader.expand(test_workspace) @@ -138,7 +140,7 @@ def test_shell_command_before(config_fixture: "WorkspaceTestData") -> None: assert test_workspace == config_fixture.shell_command_before.config_after() -def test_in_session_scope(config_fixture: "WorkspaceTestData") -> None: +def test_in_session_scope(config_fixture: WorkspaceTestData) -> None: """Verify shell_command before_session is in session scope.""" sconfig = ConfigReader._load( fmt="yaml", @@ -154,7 +156,7 @@ def test_in_session_scope(config_fixture: "WorkspaceTestData") -> None: ) -def test_trickle_relative_start_directory(config_fixture: "WorkspaceTestData") -> None: +def test_trickle_relative_start_directory(config_fixture: WorkspaceTestData) -> None: """Verify tmuxp config proliferates relative start directory to descendants.""" test_workspace = loader.trickle(config_fixture.trickle.before) assert test_workspace == config_fixture.trickle.expected @@ -179,7 +181,7 @@ def test_trickle_window_with_no_pane_workspace() -> None: } -def test_expands_blank_panes(config_fixture: "WorkspaceTestData") -> None: +def test_expands_blank_panes(config_fixture: WorkspaceTestData) -> None: """Expand blank config into full form. Handle ``NoneType`` and 'blank':: diff --git a/tests/workspace/test_finder.py b/tests/workspace/test_finder.py index 3496a2c049a..862389e29c1 100644 --- a/tests/workspace/test_finder.py +++ b/tests/workspace/test_finder.py @@ -1,5 +1,7 @@ """Test config file searching for tmuxp.""" +from __future__ import annotations + import argparse import pathlib import typing as t @@ -267,7 +269,7 @@ def config_cmd(workspace_file: str) -> None: project_config = projectdir / ".tmuxp.yaml" - def check_cmd(config_arg: str) -> "_pytest.capture.CaptureResult[str]": + def check_cmd(config_arg: str) -> _pytest.capture.CaptureResult[str]: args = parser.parse_args([config_arg]) config_cmd(workspace_file=args.workspace_file) return capsys.readouterr() diff --git a/tests/workspace/test_freezer.py b/tests/workspace/test_freezer.py index c762e2212e2..42fa6cc581c 100644 --- a/tests/workspace/test_freezer.py +++ b/tests/workspace/test_freezer.py @@ -1,17 +1,20 @@ """Tests tmux session freezing functionality for tmuxp.""" -import pathlib +from __future__ import annotations + import time import typing -from libtmux.session import Session - from tests.fixtures import utils as test_utils from tmuxp._internal.config_reader import ConfigReader from tmuxp.workspace import freezer, validation from tmuxp.workspace.builder import WorkspaceBuilder if typing.TYPE_CHECKING: + import pathlib + + from libtmux.session import Session + from tests.fixtures.structures import WorkspaceTestData @@ -87,7 +90,7 @@ def test_inline_workspace() -> None: def test_export_yaml( tmp_path: pathlib.Path, - config_fixture: "WorkspaceTestData", + config_fixture: WorkspaceTestData, ) -> None: """Test exporting a frozen tmux session to YAML.""" yaml_workspace_file = tmp_path / "config.yaml" diff --git a/tests/workspace/test_import_teamocil.py b/tests/workspace/test_import_teamocil.py index 347fe26627a..2f6ac9800fd 100644 --- a/tests/workspace/test_import_teamocil.py +++ b/tests/workspace/test_import_teamocil.py @@ -1,5 +1,7 @@ """Test for tmuxp teamocil configuration.""" +from __future__ import annotations + import typing as t import pytest diff --git a/tests/workspace/test_import_tmuxinator.py b/tests/workspace/test_import_tmuxinator.py index 086c382f83f..e6443e32041 100644 --- a/tests/workspace/test_import_tmuxinator.py +++ b/tests/workspace/test_import_tmuxinator.py @@ -1,5 +1,7 @@ """Test for tmuxp tmuxinator configuration.""" +from __future__ import annotations + import typing as t import pytest