Skip to content

Use future annotations #957

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force

<!-- Maintainers, insert changes / features for the next release here -->

### 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
Expand Down
4 changes: 3 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
https://docs.pytest.org/en/stable/deprecations.html
"""

from __future__ import annotations

import logging
import os
import pathlib
Expand All @@ -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.
Expand Down
18 changes: 10 additions & 8 deletions docs/_ext/aafig.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
:license: BOLA, see LICENSE for details
"""

from __future__ import annotations

import locale
import logging
import posixpath
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
10 changes: 6 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# flake8: NOQA: E501
"""Sphinx documentation configuration for tmuxp."""

from __future__ import annotations

import contextlib
import inspect
import pathlib
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -213,14 +215,14 @@ 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"
with contextlib.suppress(FileNotFoundError):
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)
12 changes: 12 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions src/tmuxp/__about__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Metadata for tmuxp package."""

from __future__ import annotations

__title__ = "tmuxp"
__package_name__ = "tmuxp"
__version__ = "1.50.1"
Expand Down
2 changes: 2 additions & 0 deletions src/tmuxp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
:license: MIT, see LICENSE for details
"""

from __future__ import annotations

from . import cli, util
from .__about__ import (
__author__,
Expand Down
16 changes: 9 additions & 7 deletions src/tmuxp/_internal/config_reader.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Configuration parser for YAML and JSON files."""

from __future__ import annotations

import json
import pathlib
import typing as t
Expand All @@ -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" }')
Expand All @@ -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" }')
Expand Down Expand Up @@ -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**
Expand Down Expand Up @@ -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:
Expand All @@ -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" })
Expand Down
2 changes: 2 additions & 0 deletions src/tmuxp/_internal/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
...
"""

from __future__ import annotations

from typing_extensions import NotRequired, TypedDict


Expand Down
13 changes: 8 additions & 5 deletions src/tmuxp/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 6 additions & 4 deletions src/tmuxp/cli/convert.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""CLI for ``tmuxp convert`` subcommand."""

import argparse
from __future__ import annotations

import locale
import os
import pathlib
Expand All @@ -13,6 +14,8 @@
from .utils import prompt_yes_no

if t.TYPE_CHECKING:
import argparse

AllowedFileTypes = t.Literal["json", "yaml"]


Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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}>.")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were added back in subsequent commit

8 changes: 6 additions & 2 deletions src/tmuxp/cli/debug_info.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""CLI for ``tmuxp debug-info`` subcommand."""

import argparse
from __future__ import annotations

import os
import pathlib
import platform
Expand All @@ -16,6 +17,9 @@

from .utils import tmuxp_echo

if t.TYPE_CHECKING:
import argparse

tmuxp_path = pathlib.Path(__file__).parent.parent


Expand All @@ -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."""

Expand Down
12 changes: 8 additions & 4 deletions src/tmuxp/cli/edit.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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)
Expand Down
Loading
Loading