Skip to content

Commit a471e15

Browse files
authored
chore: Implement PEP 563 deferred annotation resolution (#34)
- 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 566f350 + 5511d6d commit a471e15

File tree

7 files changed

+55
-11
lines changed

7 files changed

+55
-11
lines changed

CHANGES

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

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

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

2637
_Maintenance only, no bug fixes, or new features_

conftest.py

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

3+
from __future__ import annotations
4+
35
import pytest
46

57

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 configuration for g."""
33

4+
from __future__ import annotations
5+
46
import contextlib
57
import inspect
68
import pathlib
@@ -71,7 +73,7 @@
7173
html_extra_path = ["manifest.json"]
7274
html_theme = "furo"
7375
html_theme_path: list[str] = []
74-
html_theme_options: dict[str, t.Union[str, list[dict[str, str]]]] = {
76+
html_theme_options: dict[str, str | list[dict[str, str]]] = {
7577
"light_logo": "img/g.svg",
7678
"dark_logo": "img/g-dark.svg",
7779
"footer_icons": [
@@ -129,7 +131,7 @@
129131
}
130132

131133

132-
def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
134+
def linkcode_resolve(domain: str, info: dict[str, str]) -> None | str:
133135
"""
134136
Determine the URL corresponding to Python object.
135137
@@ -199,7 +201,7 @@ def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
199201
)
200202

201203

202-
def remove_tabs_js(app: "Sphinx", exc: Exception) -> None:
204+
def remove_tabs_js(app: Sphinx, exc: Exception) -> None:
203205
"""Remove tabs.js from _static after build."""
204206
# Fix for sphinx-inline-tabs#18
205207
if app.builder.format == "html" and not exc:
@@ -208,6 +210,6 @@ def remove_tabs_js(app: "Sphinx", exc: Exception) -> None:
208210
tabs_js.unlink() # When python 3.7 deprecated, use missing_ok=True
209211

210212

211-
def setup(app: "Sphinx") -> None:
213+
def setup(app: Sphinx) -> None:
212214
"""Configure Sphinx app hooks."""
213215
app.connect("build-finished", remove_tabs_js)

pyproject.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,16 @@ select = [
152152
"PERF", # Perflint
153153
"RUF", # Ruff-specific rules
154154
"D", # pydocstyle
155+
"FA100", # future annotations
155156
]
156157
ignore = [
157158
"COM812", # missing trailing comma, ruff format conflict
158159
]
160+
extend-safe-fixes = [
161+
"UP006",
162+
"UP007",
163+
]
164+
pyupgrade.keep-runtime-typing = false
159165

160166
[tool.ruff.lint.pydocstyle]
161167
convention = "numpy"
@@ -165,6 +171,9 @@ known-first-party = [
165171
"g",
166172
]
167173
combine-as-imports = true
174+
required-imports = [
175+
"from __future__ import annotations",
176+
]
168177

169178
[tool.ruff.lint.per-file-ignores]
170179
"*/__init__.py" = ["F401"]
@@ -184,3 +193,15 @@ filterwarnings = [
184193
[tool.pytest-watcher]
185194
now = true
186195
ignore_patterns = ["*.py.*.py"]
196+
197+
[tool.coverage.report]
198+
exclude_also = [
199+
"def __repr__",
200+
"raise AssertionError",
201+
"raise NotImplementedError",
202+
"if __name__ == .__main__.:",
203+
"if TYPE_CHECKING:",
204+
"class .*\\bProtocol\\):",
205+
"@(abc\\.)?abstractmethod",
206+
"from __future__ import annotations",
207+
]

src/g/__about__.py

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

3+
from __future__ import annotations
4+
35
__title__ = "g"
46
__package_name__ = "g"
57
__description__ = "CLI alias for your current directory's VCS command: git, svn, hg"

src/g/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#!/usr/bin/env python
22
"""Package for g."""
33

4+
from __future__ import annotations
5+
46
import io
57
import logging
68
import os
@@ -17,7 +19,7 @@
1719
log = logging.getLogger(__name__)
1820

1921

20-
def find_repo_type(path: t.Union[pathlib.Path, str]) -> t.Optional[str]:
22+
def find_repo_type(path: pathlib.Path | str) -> str | None:
2123
"""Detect repo type looking upwards."""
2224
for _path in [*list(pathlib.Path(path).parents), pathlib.Path(path)]:
2325
for p in _path.iterdir():
@@ -30,12 +32,12 @@ def find_repo_type(path: t.Union[pathlib.Path, str]) -> t.Optional[str]:
3032

3133

3234
def run(
33-
cmd: t.Union[str, bytes, "PathLike[str]", "PathLike[bytes]", object] = DEFAULT,
35+
cmd: str | bytes | PathLike[str] | PathLike[bytes] | object = DEFAULT,
3436
cmd_args: object = DEFAULT,
3537
wait: bool = False,
3638
*args: object,
3739
**kwargs: t.Any,
38-
) -> t.Optional["subprocess.Popen[str]"]:
40+
) -> subprocess.Popen[str] | None:
3941
"""CLI Entrypoint for g, overlay for current directory's VCS utility.
4042
4143
Environment variables

tests/test_cli.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Tests for g's CLI package."""
22

3+
from __future__ import annotations
4+
35
import enum
4-
import pathlib
56
import subprocess
67
import typing as t
78
from unittest.mock import patch
@@ -10,11 +11,14 @@
1011

1112
from g import run
1213

14+
if t.TYPE_CHECKING:
15+
import pathlib
16+
1317

1418
def get_output(
1519
*args: t.Any,
1620
**kwargs: t.Any,
17-
) -> t.Union[subprocess.CalledProcessError, t.Any]:
21+
) -> subprocess.CalledProcessError | t.Any:
1822
"""Retrieve output from CLI subprocess, whether success or error."""
1923
try:
2024
return subprocess.check_output(*args, **kwargs)
@@ -49,7 +53,7 @@ class CommandLineTestFixture(t.NamedTuple):
4953
argv_args: list[str]
5054

5155
# results
52-
expect_cmd: t.Optional[str]
56+
expect_cmd: str | None
5357

5458

5559
TEST_FIXTURES: list[CommandLineTestFixture] = [
@@ -90,7 +94,7 @@ def test_command_line(
9094
test_id: str,
9195
env: EnvFlag,
9296
argv_args: list[str],
93-
expect_cmd: t.Optional[str],
97+
expect_cmd: str | None,
9498
monkeypatch: pytest.MonkeyPatch,
9599
tmp_path: pathlib.Path,
96100
) -> None:

0 commit comments

Comments
 (0)