Skip to content

Commit 224f484

Browse files
authored
chore: Implement PEP 563 deferred annotation resolution (#459)
- 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/
2 parents dd45294 + eb80806 commit 224f484

25 files changed

+184
-92
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 'vcspull' --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 (#459)
27+
28+
- Add `from __future__ import annotations` to defer annotation resolution and reduce unnecessary runtime 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
## vcspull v1.33.0 (2024-11-23)
2536

2637
_Maintenance only, no bug fixes, or new features_

conftest.py

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

11-
import pathlib
11+
from __future__ import annotations
12+
1213
import shutil
1314
import typing as t
1415

1516
import pytest
1617

18+
if t.TYPE_CHECKING:
19+
import pathlib
20+
1721

1822
@pytest.fixture(autouse=True)
1923
def add_doctest_fixtures(

docs/conf.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Sphinx configuration for vcspull documentation."""
22

33
# flake8: noqa: E501
4+
from __future__ import annotations
5+
46
import inspect
57
import pathlib
68
import sys
@@ -146,7 +148,7 @@
146148
}
147149

148150

149-
def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
151+
def linkcode_resolve(domain: str, info: dict[str, str]) -> None | str:
150152
"""
151153
Determine the URL corresponding to Python object.
152154
@@ -216,13 +218,13 @@ def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
216218
)
217219

218220

219-
def remove_tabs_js(app: "Sphinx", exc: Exception) -> None:
221+
def remove_tabs_js(app: Sphinx, exc: Exception) -> None:
220222
"""Fix for sphinx-inline-tabs#18."""
221223
if app.builder.format == "html" and not exc:
222224
tabs_js = pathlib.Path(app.builder.outdir) / "_static" / "tabs.js"
223225
tabs_js.unlink(missing_ok=True)
224226

225227

226-
def setup(app: "Sphinx") -> None:
228+
def setup(app: Sphinx) -> None:
227229
"""Sphinx setup hook."""
228230
app.connect("build-finished", remove_tabs_js)

pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ exclude_lines = [
178178
"if TYPE_CHECKING:",
179179
"if t.TYPE_CHECKING:",
180180
"@overload( |$)",
181+
"from __future__ import annotations",
181182
]
182183

183184
[tool.ruff]
@@ -201,10 +202,16 @@ select = [
201202
"PERF", # Perflint
202203
"RUF", # Ruff-specific rules
203204
"D", # pydocstyle
205+
"FA100", # future annotations
204206
]
205207
ignore = [
206208
"COM812", # missing trailing comma, ruff format conflict
207209
]
210+
extend-safe-fixes = [
211+
"UP006",
212+
"UP007",
213+
]
214+
pyupgrade.keep-runtime-typing = false
208215

209216
[tool.ruff.lint.pydocstyle]
210217
convention = "numpy"
@@ -214,6 +221,9 @@ known-first-party = [
214221
"vcspull",
215222
]
216223
combine-as-imports = true
224+
required-imports = [
225+
"from __future__ import annotations",
226+
]
217227

218228
[tool.ruff.lint.per-file-ignores]
219229
"*/__init__.py" = ["F401"]

scripts/generate_gitlab.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
#!/usr/bin/env python
22
"""Example script for export gitlab organization to vcspull config file."""
33

4+
from __future__ import annotations
5+
46
import argparse
57
import logging
68
import os
79
import pathlib
810
import sys
11+
import typing as t
912

1013
import requests
1114
import yaml
1215
from libvcs.sync.git import GitRemote
1316

1417
from vcspull.cli.sync import CouldNotGuessVCSFromURL, guess_vcs
15-
from vcspull.types import RawConfig
18+
19+
if t.TYPE_CHECKING:
20+
from vcspull.types import RawConfig
1621

1722
log = logging.getLogger(__name__)
1823
logging.basicConfig(level=logging.INFO, format="%(message)s")

src/vcspull/__about__.py

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

3+
from __future__ import annotations
4+
35
__title__ = "vcspull"
46
__package_name__ = "vcspull"
57
__description__ = "Manage and sync multiple git, mercurial, and svn repos"

src/vcspull/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
"""
77

88
# Set default logging handler to avoid "No handler found" warnings.
9+
from __future__ import annotations
10+
911
import logging
1012
from logging import NullHandler
1113

src/vcspull/_internal/config_reader.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import json
24
import pathlib
35
import typing as t
@@ -22,11 +24,11 @@ class ConfigReader:
2224
'{\n "session_name": "my session"\n}'
2325
"""
2426

25-
def __init__(self, content: "RawConfigData") -> None:
27+
def __init__(self, content: RawConfigData) -> None:
2628
self.content = content
2729

2830
@staticmethod
29-
def _load(fmt: "FormatLiteral", content: str) -> dict[str, t.Any]:
31+
def _load(fmt: FormatLiteral, content: str) -> dict[str, t.Any]:
3032
"""Load raw config data and directly return it.
3133
3234
>>> ConfigReader._load("json", '{ "session_name": "my session" }')
@@ -49,7 +51,7 @@ def _load(fmt: "FormatLiteral", content: str) -> dict[str, t.Any]:
4951
raise NotImplementedError(msg)
5052

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

120122
@classmethod
121-
def from_file(cls, path: pathlib.Path) -> "ConfigReader":
123+
def from_file(cls, path: pathlib.Path) -> ConfigReader:
122124
r"""Load data from file path.
123125
124126
**YAML file**
@@ -159,8 +161,8 @@ def from_file(cls, path: pathlib.Path) -> "ConfigReader":
159161

160162
@staticmethod
161163
def _dump(
162-
fmt: "FormatLiteral",
163-
content: "RawConfigData",
164+
fmt: FormatLiteral,
165+
content: RawConfigData,
164166
indent: int = 2,
165167
**kwargs: t.Any,
166168
) -> str:
@@ -187,7 +189,7 @@ def _dump(
187189
msg = f"{fmt} not supported in config"
188190
raise NotImplementedError(msg)
189191

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

src/vcspull/cli/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""CLI utilities for vcspull."""
22

3+
from __future__ import annotations
4+
35
import argparse
46
import logging
57
import textwrap
@@ -41,7 +43,7 @@ def create_parser(return_subparsers: t.Literal[False]) -> argparse.ArgumentParse
4143

4244
def create_parser(
4345
return_subparsers: bool = False,
44-
) -> t.Union[argparse.ArgumentParser, tuple[argparse.ArgumentParser, t.Any]]:
46+
) -> argparse.ArgumentParser | tuple[argparse.ArgumentParser, t.Any]:
4547
"""Create CLI argument parser for vcspull."""
4648
parser = argparse.ArgumentParser(
4749
prog="vcspull",
@@ -76,7 +78,7 @@ def create_parser(
7678
return parser
7779

7880

79-
def cli(_args: t.Optional[list[str]] = None) -> None:
81+
def cli(_args: list[str] | None = None) -> None:
8082
"""CLI entry point for vcspull."""
8183
parser, sync_parser = create_parser(return_subparsers=True)
8284
args = parser.parse_args(_args)

src/vcspull/cli/sync.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
"""Synchronization functionality for vcspull."""
22

3-
import argparse
3+
from __future__ import annotations
4+
45
import logging
5-
import pathlib
66
import sys
77
import typing as t
88
from copy import deepcopy
9-
from datetime import datetime
109

1110
from libvcs._internal.shortcuts import create_project
12-
from libvcs._internal.types import VCSLiteral
13-
from libvcs.sync.git import GitSync
1411
from libvcs.url import registry as url_tools
1512

1613
from vcspull import exc
1714
from vcspull.config import filter_repos, find_config_files, load_configs
1815

16+
if t.TYPE_CHECKING:
17+
import argparse
18+
import pathlib
19+
from datetime import datetime
20+
21+
from libvcs._internal.types import VCSLiteral
22+
from libvcs.sync.git import GitSync
23+
1924
log = logging.getLogger(__name__)
2025

2126

@@ -63,9 +68,8 @@ def sync(
6368
repo_patterns: list[str],
6469
config: pathlib.Path,
6570
exit_on_error: bool,
66-
parser: t.Optional[
67-
argparse.ArgumentParser
68-
] = None, # optional so sync can be unit tested
71+
parser: argparse.ArgumentParser
72+
| None = None, # optional so sync can be unit tested
6973
) -> None:
7074
"""Entry point for ``vcspull sync``."""
7175
if isinstance(repo_patterns, list) and len(repo_patterns) == 0:
@@ -117,7 +121,7 @@ def progress_cb(output: str, timestamp: datetime) -> None:
117121
sys.stdout.flush()
118122

119123

120-
def guess_vcs(url: str) -> t.Optional[VCSLiteral]:
124+
def guess_vcs(url: str) -> VCSLiteral | None:
121125
"""Guess the VCS from a URL."""
122126
vcs_matches = url_tools.registry.match(url=url, is_explicit=True)
123127

0 commit comments

Comments
 (0)