Skip to content

Commit 0d73f64

Browse files
authored
chore: Implement PEP 563 deferred annotation resolution (#957)
- Add `from __future__ import annotations` to defer annotation resolution and reduce unnecessary symbol 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/
2 parents 0aa1c5d + 5b04bf4 commit 0d73f64

Some content is hidden

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

85 files changed

+488
-278
lines changed

CHANGES

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force
1919

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

22+
### Development
23+
24+
#### chore: Implement PEP 563 deferred annotation resolution (#957)
25+
26+
- Add `from __future__ import annotations` to defer annotation resolution and reduce unnecessary runtime computations during type checking.
27+
- Enable Ruff checks for PEP-compliant annotations:
28+
- [non-pep585-annotation (UP006)](https://docs.astral.sh/ruff/rules/non-pep585-annotation/)
29+
- [non-pep604-annotation (UP007)](https://docs.astral.sh/ruff/rules/non-pep604-annotation/)
30+
31+
For more details on PEP 563, see: https://peps.python.org/pep-0563/
32+
2233
## tmuxp 1.50.1 (2024-12-24)
2334

2435
### Development

conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
https://docs.pytest.org/en/stable/deprecations.html
99
"""
1010

11+
from __future__ import annotations
12+
1113
import logging
1214
import os
1315
import pathlib
@@ -29,7 +31,7 @@
2931

3032

3133
@pytest.fixture(autouse=USING_ZSH, scope="session")
32-
def zshrc(user_path: pathlib.Path) -> t.Optional[pathlib.Path]:
34+
def zshrc(user_path: pathlib.Path) -> pathlib.Path | None:
3335
"""Quiets ZSH default message.
3436
3537
Needs a startup file .zshenv, .zprofile, .zshrc, .zlogin.

docs/_ext/aafig.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
:license: BOLA, see LICENSE for details
1313
"""
1414

15+
from __future__ import annotations
16+
1517
import locale
1618
import logging
1719
import posixpath
@@ -40,9 +42,9 @@
4042

4143

4244
def merge_dict(
43-
dst: dict[str, t.Optional[str]],
44-
src: dict[str, t.Optional[str]],
45-
) -> dict[str, t.Optional[str]]:
45+
dst: dict[str, str | None],
46+
src: dict[str, str | None],
47+
) -> dict[str, str | None]:
4648
for k, v in src.items():
4749
if k not in dst:
4850
dst[k] = v
@@ -52,7 +54,7 @@ def merge_dict(
5254
def get_basename(
5355
text: str,
5456
options: dict[str, str],
55-
prefix: t.Optional[str] = "aafig",
57+
prefix: str | None = "aafig",
5658
) -> str:
5759
options = options.copy()
5860
if "format" in options:
@@ -105,7 +107,7 @@ def run(self) -> list[nodes.Node]:
105107
return [image_node]
106108

107109

108-
def render_aafig_images(app: "Sphinx", doctree: nodes.Node) -> None:
110+
def render_aafig_images(app: Sphinx, doctree: nodes.Node) -> None:
109111
format_map = app.builder.config.aafig_format
110112
merge_dict(format_map, DEFAULT_FORMATS)
111113
if aafigure is None:
@@ -157,10 +159,10 @@ def __init__(self, *args: object, **kwargs: object) -> None:
157159

158160

159161
def render_aafigure(
160-
app: "Sphinx",
162+
app: Sphinx,
161163
text: str,
162164
options: dict[str, str],
163-
) -> tuple[str, str, t.Optional[str], t.Optional[str]]:
165+
) -> tuple[str, str, str | None, str | None]:
164166
"""Render an ASCII art figure into the requested format output file."""
165167
if aafigure is None:
166168
raise AafigureNotInstalled
@@ -227,7 +229,7 @@ def render_aafigure(
227229
return relfn, outfn, None, extra
228230

229231

230-
def setup(app: "Sphinx") -> None:
232+
def setup(app: Sphinx) -> None:
231233
app.add_directive("aafig", AafigDirective)
232234
app.connect("doctree-read", render_aafig_images)
233235
app.add_config_value("aafig_format", DEFAULT_FORMATS, "html")

docs/conf.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# flake8: NOQA: E501
22
"""Sphinx documentation configuration for tmuxp."""
33

4+
from __future__ import annotations
5+
46
import contextlib
57
import inspect
68
import pathlib
@@ -74,7 +76,7 @@
7476
html_favicon = "_static/favicon.ico"
7577
html_theme = "furo"
7678
html_theme_path: list[str] = []
77-
html_theme_options: dict[str, t.Union[str, list[dict[str, str]]]] = {
79+
html_theme_options: dict[str, str | list[dict[str, str]]] = {
7880
"light_logo": "img/tmuxp.svg",
7981
"dark_logo": "img/tmuxp.svg",
8082
"footer_icons": [
@@ -143,7 +145,7 @@
143145
}
144146

145147

146-
def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
148+
def linkcode_resolve(domain: str, info: dict[str, str]) -> None | str:
147149
"""
148150
Determine the URL corresponding to Python object.
149151
@@ -213,14 +215,14 @@ def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
213215
)
214216

215217

216-
def remove_tabs_js(app: "Sphinx", exc: Exception) -> None:
218+
def remove_tabs_js(app: Sphinx, exc: Exception) -> None:
217219
"""Fix for sphinx-inline-tabs#18."""
218220
if app.builder.format == "html" and not exc:
219221
tabs_js = pathlib.Path(app.builder.outdir) / "_static" / "tabs.js"
220222
with contextlib.suppress(FileNotFoundError):
221223
tabs_js.unlink() # When python 3.7 deprecated, use missing_ok=True
222224

223225

224-
def setup(app: "Sphinx") -> None:
226+
def setup(app: Sphinx) -> None:
225227
"""Sphinx setup hook."""
226228
app.connect("build-finished", remove_tabs_js)

pyproject.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ exclude_lines = [
149149
"raise NotImplementedError",
150150
"if __name__ == .__main__.:",
151151
"def parse_args",
152+
"from __future__ import annotations",
153+
"if TYPE_CHECKING:",
154+
"if t.TYPE_CHECKING:",
152155
]
153156

154157
[tool.mypy]
@@ -192,16 +195,25 @@ select = [
192195
"PERF", # Perflint
193196
"RUF", # Ruff-specific rules
194197
"D", # pydocstyle
198+
"FA100", # future annotations
195199
]
196200
ignore = [
197201
"COM812", # missing trailing comma, ruff format conflict
198202
]
203+
extend-safe-fixes = [
204+
"UP006",
205+
"UP007",
206+
]
207+
pyupgrade.keep-runtime-typing = false
199208

200209
[tool.ruff.lint.isort]
201210
known-first-party = [
202211
"tmuxp",
203212
]
204213
combine-as-imports = true
214+
required-imports = [
215+
"from __future__ import annotations",
216+
]
205217

206218
[tool.ruff.lint.pydocstyle]
207219
convention = "numpy"

src/tmuxp/__about__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Metadata for tmuxp package."""
22

3+
from __future__ import annotations
4+
35
__title__ = "tmuxp"
46
__package_name__ = "tmuxp"
57
__version__ = "1.50.1"

src/tmuxp/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
:license: MIT, see LICENSE for details
66
"""
77

8+
from __future__ import annotations
9+
810
from . import cli, util
911
from .__about__ import (
1012
__author__,

src/tmuxp/_internal/config_reader.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Configuration parser for YAML and JSON files."""
22

3+
from __future__ import annotations
4+
35
import json
46
import pathlib
57
import typing as t
@@ -24,11 +26,11 @@ class ConfigReader:
2426
'{\n "session_name": "my session"\n}'
2527
"""
2628

27-
def __init__(self, content: "RawConfigData") -> None:
29+
def __init__(self, content: RawConfigData) -> None:
2830
self.content = content
2931

3032
@staticmethod
31-
def _load(fmt: "FormatLiteral", content: str) -> dict[str, t.Any]:
33+
def _load(fmt: FormatLiteral, content: str) -> dict[str, t.Any]:
3234
"""Load raw config data and directly return it.
3335
3436
>>> ConfigReader._load("json", '{ "session_name": "my session" }')
@@ -51,7 +53,7 @@ def _load(fmt: "FormatLiteral", content: str) -> dict[str, t.Any]:
5153
raise NotImplementedError(msg)
5254

5355
@classmethod
54-
def load(cls, fmt: "FormatLiteral", content: str) -> "ConfigReader":
56+
def load(cls, fmt: FormatLiteral, content: str) -> ConfigReader:
5557
"""Load raw config data into a ConfigReader instance (to dump later).
5658
5759
>>> cfg = ConfigReader.load("json", '{ "session_name": "my session" }')
@@ -120,7 +122,7 @@ def _from_file(cls, path: pathlib.Path) -> dict[str, t.Any]:
120122
)
121123

122124
@classmethod
123-
def from_file(cls, path: pathlib.Path) -> "ConfigReader":
125+
def from_file(cls, path: pathlib.Path) -> ConfigReader:
124126
r"""Load data from file path.
125127
126128
**YAML file**
@@ -161,8 +163,8 @@ def from_file(cls, path: pathlib.Path) -> "ConfigReader":
161163

162164
@staticmethod
163165
def _dump(
164-
fmt: "FormatLiteral",
165-
content: "RawConfigData",
166+
fmt: FormatLiteral,
167+
content: RawConfigData,
166168
indent: int = 2,
167169
**kwargs: t.Any,
168170
) -> str:
@@ -189,7 +191,7 @@ def _dump(
189191
msg = f"{fmt} not supported in config"
190192
raise NotImplementedError(msg)
191193

192-
def dump(self, fmt: "FormatLiteral", indent: int = 2, **kwargs: t.Any) -> str:
194+
def dump(self, fmt: FormatLiteral, indent: int = 2, **kwargs: t.Any) -> str:
193195
r"""Dump via ConfigReader instance.
194196
195197
>>> cfg = ConfigReader({ "session_name": "my session" })

src/tmuxp/_internal/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
...
1111
"""
1212

13+
from __future__ import annotations
14+
1315
from typing_extensions import NotRequired, TypedDict
1416

1517

src/tmuxp/cli/__init__.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""CLI utilities for tmuxp."""
22

3+
from __future__ import annotations
4+
35
import argparse
46
import logging
57
import os
6-
import pathlib
78
import sys
89
import typing as t
910

@@ -32,6 +33,8 @@
3233
logger = logging.getLogger(__name__)
3334

3435
if t.TYPE_CHECKING:
36+
import pathlib
37+
3538
from typing_extensions import TypeAlias
3639

3740
CLIVerbosity: TypeAlias = t.Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
@@ -109,16 +112,16 @@ def create_parser() -> argparse.ArgumentParser:
109112
class CLINamespace(argparse.Namespace):
110113
"""Typed :class:`argparse.Namespace` for tmuxp root-level CLI."""
111114

112-
log_level: "CLIVerbosity"
113-
subparser_name: "CLISubparserName"
114-
import_subparser_name: t.Optional["CLIImportSubparserName"]
115+
log_level: CLIVerbosity
116+
subparser_name: CLISubparserName
117+
import_subparser_name: CLIImportSubparserName | None
115118
version: bool
116119

117120

118121
ns = CLINamespace()
119122

120123

121-
def cli(_args: t.Optional[list[str]] = None) -> None:
124+
def cli(_args: list[str] | None = None) -> None:
122125
"""Manage tmux sessions.
123126
124127
Pass the "--help" argument to any command to see detailed help.

src/tmuxp/cli/convert.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""CLI for ``tmuxp convert`` subcommand."""
22

3-
import argparse
3+
from __future__ import annotations
4+
45
import locale
56
import os
67
import pathlib
@@ -13,6 +14,8 @@
1314
from .utils import prompt_yes_no
1415

1516
if t.TYPE_CHECKING:
17+
import argparse
18+
1619
AllowedFileTypes = t.Literal["json", "yaml"]
1720

1821

@@ -53,9 +56,9 @@ def __init__(self, ext: str, *args: object, **kwargs: object) -> None:
5356

5457

5558
def command_convert(
56-
workspace_file: t.Union[str, pathlib.Path],
59+
workspace_file: str | pathlib.Path,
5760
answer_yes: bool,
58-
parser: t.Optional[argparse.ArgumentParser] = None,
61+
parser: argparse.ArgumentParser | None = None,
5962
) -> None:
6063
"""Entrypoint for ``tmuxp convert`` convert a tmuxp config between JSON and YAML."""
6164
workspace_file = find_workspace_file(
@@ -95,4 +98,3 @@ def command_convert(
9598
if answer_yes:
9699
with open(newfile, "w", encoding=locale.getpreferredencoding(False)) as buf:
97100
buf.write(new_workspace)
98-
print(f"New workspace file saved to <{newfile}>.")

src/tmuxp/cli/debug_info.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""CLI for ``tmuxp debug-info`` subcommand."""
22

3-
import argparse
3+
from __future__ import annotations
4+
45
import os
56
import pathlib
67
import platform
@@ -16,6 +17,9 @@
1617

1718
from .utils import tmuxp_echo
1819

20+
if t.TYPE_CHECKING:
21+
import argparse
22+
1923
tmuxp_path = pathlib.Path(__file__).parent.parent
2024

2125

@@ -27,7 +31,7 @@ def create_debug_info_subparser(
2731

2832

2933
def command_debug_info(
30-
parser: t.Optional[argparse.ArgumentParser] = None,
34+
parser: argparse.ArgumentParser | None = None,
3135
) -> None:
3236
"""Entrypoint for ``tmuxp debug-info`` to print debug info to submit with issues."""
3337

src/tmuxp/cli/edit.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
"""CLI for ``tmuxp edit`` subcommand."""
22

3-
import argparse
3+
from __future__ import annotations
4+
45
import os
5-
import pathlib
66
import subprocess
77
import typing as t
88

99
from tmuxp.workspace.finders import find_workspace_file
1010

11+
if t.TYPE_CHECKING:
12+
import argparse
13+
import pathlib
14+
1115

1216
def create_edit_subparser(
1317
parser: argparse.ArgumentParser,
@@ -23,8 +27,8 @@ def create_edit_subparser(
2327

2428

2529
def command_edit(
26-
workspace_file: t.Union[str, pathlib.Path],
27-
parser: t.Optional[argparse.ArgumentParser] = None,
30+
workspace_file: str | pathlib.Path,
31+
parser: argparse.ArgumentParser | None = None,
2832
) -> None:
2933
"""Entrypoint for ``tmuxp edit``, open tmuxp workspace file in system editor."""
3034
workspace_file = find_workspace_file(workspace_file)

0 commit comments

Comments
 (0)