Skip to content

Commit e147713

Browse files
authored
chore: Implement PEP 563 deferred annotation resolution (#483)
- 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 d286f51 + 969dc1c commit e147713

Some content is hidden

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

43 files changed

+1221
-1090
lines changed

CHANGES

+11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ $ pip install --user --upgrade --pre libvcs
1515

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

18+
### Development
19+
20+
#### chore: Implement PEP 563 deferred annotation resolution (#483)
21+
22+
- Add `from __future__ import annotations` to defer annotation resolution and reduce unnecessary runtime computations during type checking.
23+
- Enable Ruff checks for PEP-compliant annotations:
24+
- [non-pep585-annotation (UP006)](https://docs.astral.sh/ruff/rules/non-pep585-annotation/)
25+
- [non-pep604-annotation (UP007)](https://docs.astral.sh/ruff/rules/non-pep604-annotation/)
26+
27+
For more details on PEP 563, see: https://peps.python.org/pep-0563/
28+
1829
## libvcs 0.34.0 (2024-11-22)
1930

2031
_Maintenance only, no bug fixes, or new features_

conftest.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@
88
https://docs.pytest.org/en/stable/deprecations.html
99
"""
1010

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

1415
import pytest
1516

17+
if t.TYPE_CHECKING:
18+
import pathlib
19+
1620
pytest_plugins = ["pytester"]
1721

1822

docs/conf.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# flake8: NOQA: E501
22
"""Sphinx configuration for libvcs."""
33

4+
from __future__ import annotations
5+
46
import inspect
57
import pathlib
68
import sys
@@ -71,7 +73,7 @@
7173
html_favicon = "_static/favicon.ico"
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/libvcs.svg",
7678
"dark_logo": "img/libvcs-dark.svg",
7779
"footer_icons": [
@@ -150,7 +152,7 @@
150152
}
151153

152154

153-
def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
155+
def linkcode_resolve(domain: str, info: dict[str, str]) -> None | str:
154156
"""
155157
Determine the URL corresponding to Python object.
156158
@@ -220,14 +222,14 @@ def linkcode_resolve(domain: str, info: dict[str, str]) -> t.Union[None, str]:
220222
)
221223

222224

223-
def remove_tabs_js(app: "Sphinx", exc: Exception) -> None:
225+
def remove_tabs_js(app: Sphinx, exc: Exception) -> None:
224226
"""Remove tabs.js from _static after build."""
225227
# Fix for sphinx-inline-tabs#18
226228
if app.builder.format == "html" and not exc:
227229
tabs_js = pathlib.Path(app.builder.outdir) / "_static" / "tabs.js"
228230
tabs_js.unlink(missing_ok=True)
229231

230232

231-
def setup(app: "Sphinx") -> None:
233+
def setup(app: Sphinx) -> None:
232234
"""Configure Sphinx app hooks."""
233235
app.connect("build-finished", remove_tabs_js)

pyproject.toml

+10
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ exclude_lines = [
154154
"if TYPE_CHECKING:",
155155
"if t.TYPE_CHECKING:",
156156
"@overload( |$)",
157+
"from __future__ import annotations",
157158
]
158159

159160
[tool.ruff]
@@ -177,16 +178,25 @@ select = [
177178
"PERF", # Perflint
178179
"RUF", # Ruff-specific rules
179180
"D", # pydocstyle
181+
"FA100", # future annotations
180182
]
181183
ignore = [
182184
"COM812", # missing trailing comma, ruff format conflict
183185
]
186+
extend-safe-fixes = [
187+
"UP006",
188+
"UP007",
189+
]
190+
pyupgrade.keep-runtime-typing = false
184191

185192
[tool.ruff.lint.isort]
186193
known-first-party = [
187194
"libvcs",
188195
]
189196
combine-as-imports = true
197+
required-imports = [
198+
"from __future__ import annotations",
199+
]
190200

191201
[tool.ruff.lint.pydocstyle]
192202
convention = "numpy"

src/libvcs/__about__.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Metadata package for libvcs."""
22

3+
from __future__ import annotations
4+
35
__title__ = "libvcs"
46
__package_name__ = "libvcs"
57
__description__ = "Lite, typed, python utilities for Git, SVN, Mercurial, etc."

src/libvcs/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Project package for libvcs."""
22

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

57
from ._internal.run import CmdLoggingAdapter

src/libvcs/_internal/dataclasses.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
This is an internal API not covered by versioning policy.
66
"""
77

8+
from __future__ import annotations
9+
810
import dataclasses
911
import typing as t
1012
from operator import attrgetter
@@ -78,7 +80,7 @@ class SkipDefaultFieldsReprMixin:
7880
ItemWithMixin(name=Test, unit_price=2.05)
7981
"""
8082

81-
def __repr__(self: "DataclassInstance") -> str:
83+
def __repr__(self: DataclassInstance) -> str:
8284
"""Omit default fields in object representation."""
8385
nodef_f_vals = (
8486
(f.name, attrgetter(f.name)(self))

src/libvcs/_internal/module_loading.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import sys
24
import typing as t
35

src/libvcs/_internal/query_list.py

+38-36
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
This is an internal API not covered by versioning policy.
66
"""
77

8+
from __future__ import annotations
9+
810
import logging
911
import re
1012
import traceback
@@ -28,7 +30,7 @@ class ObjectDoesNotExist(Exception):
2830
def keygetter(
2931
obj: Mapping[str, t.Any],
3032
path: str,
31-
) -> t.Union[None, t.Any, str, list[str], Mapping[str, str]]:
33+
) -> None | t.Any | str | list[str] | Mapping[str, str]:
3234
"""Fetch values in objects and keys, supported nested data.
3335
3436
**With dictionaries**:
@@ -94,7 +96,7 @@ def keygetter(
9496
return dct
9597

9698

97-
def parse_lookup(obj: Mapping[str, t.Any], path: str, lookup: str) -> t.Optional[t.Any]:
99+
def parse_lookup(obj: Mapping[str, t.Any], path: str, lookup: str) -> t.Any | None:
98100
"""Check if field lookup key, e.g. "my__path__contains" has comparator, return val.
99101
100102
If comparator not used or value not found, return None.
@@ -134,23 +136,23 @@ class LookupProtocol(t.Protocol):
134136

135137
def __call__(
136138
self,
137-
data: t.Union[str, list[str], Mapping[str, str]],
138-
rhs: t.Union[str, list[str], Mapping[str, str], re.Pattern[str]],
139+
data: str | list[str] | Mapping[str, str],
140+
rhs: str | list[str] | Mapping[str, str] | re.Pattern[str],
139141
) -> bool:
140142
"""Return callback for :class:`QueryList` filtering operators."""
141143
...
142144

143145

144146
def lookup_exact(
145-
data: t.Union[str, list[str], Mapping[str, str]],
146-
rhs: t.Union[str, list[str], Mapping[str, str], re.Pattern[str]],
147+
data: str | list[str] | Mapping[str, str],
148+
rhs: str | list[str] | Mapping[str, str] | re.Pattern[str],
147149
) -> bool:
148150
return rhs == data
149151

150152

151153
def lookup_iexact(
152-
data: t.Union[str, list[str], Mapping[str, str]],
153-
rhs: t.Union[str, list[str], Mapping[str, str], re.Pattern[str]],
154+
data: str | list[str] | Mapping[str, str],
155+
rhs: str | list[str] | Mapping[str, str] | re.Pattern[str],
154156
) -> bool:
155157
if not isinstance(rhs, str) or not isinstance(data, str):
156158
return False
@@ -159,8 +161,8 @@ def lookup_iexact(
159161

160162

161163
def lookup_contains(
162-
data: t.Union[str, list[str], Mapping[str, str]],
163-
rhs: t.Union[str, list[str], Mapping[str, str], re.Pattern[str]],
164+
data: str | list[str] | Mapping[str, str],
165+
rhs: str | list[str] | Mapping[str, str] | re.Pattern[str],
164166
) -> bool:
165167
if not isinstance(rhs, str) or not isinstance(data, (str, Mapping, list)):
166168
return False
@@ -169,8 +171,8 @@ def lookup_contains(
169171

170172

171173
def lookup_icontains(
172-
data: t.Union[str, list[str], Mapping[str, str]],
173-
rhs: t.Union[str, list[str], Mapping[str, str], re.Pattern[str]],
174+
data: str | list[str] | Mapping[str, str],
175+
rhs: str | list[str] | Mapping[str, str] | re.Pattern[str],
174176
) -> bool:
175177
if not isinstance(rhs, str) or not isinstance(data, (str, Mapping, list)):
176178
return False
@@ -184,8 +186,8 @@ def lookup_icontains(
184186

185187

186188
def lookup_startswith(
187-
data: t.Union[str, list[str], Mapping[str, str]],
188-
rhs: t.Union[str, list[str], Mapping[str, str], re.Pattern[str]],
189+
data: str | list[str] | Mapping[str, str],
190+
rhs: str | list[str] | Mapping[str, str] | re.Pattern[str],
189191
) -> bool:
190192
if not isinstance(rhs, str) or not isinstance(data, str):
191193
return False
@@ -194,8 +196,8 @@ def lookup_startswith(
194196

195197

196198
def lookup_istartswith(
197-
data: t.Union[str, list[str], Mapping[str, str]],
198-
rhs: t.Union[str, list[str], Mapping[str, str], re.Pattern[str]],
199+
data: str | list[str] | Mapping[str, str],
200+
rhs: str | list[str] | Mapping[str, str] | re.Pattern[str],
199201
) -> bool:
200202
if not isinstance(rhs, str) or not isinstance(data, str):
201203
return False
@@ -204,8 +206,8 @@ def lookup_istartswith(
204206

205207

206208
def lookup_endswith(
207-
data: t.Union[str, list[str], Mapping[str, str]],
208-
rhs: t.Union[str, list[str], Mapping[str, str], re.Pattern[str]],
209+
data: str | list[str] | Mapping[str, str],
210+
rhs: str | list[str] | Mapping[str, str] | re.Pattern[str],
209211
) -> bool:
210212
if not isinstance(rhs, str) or not isinstance(data, str):
211213
return False
@@ -214,17 +216,17 @@ def lookup_endswith(
214216

215217

216218
def lookup_iendswith(
217-
data: t.Union[str, list[str], Mapping[str, str]],
218-
rhs: t.Union[str, list[str], Mapping[str, str], re.Pattern[str]],
219+
data: str | list[str] | Mapping[str, str],
220+
rhs: str | list[str] | Mapping[str, str] | re.Pattern[str],
219221
) -> bool:
220222
if not isinstance(rhs, str) or not isinstance(data, str):
221223
return False
222224
return data.lower().endswith(rhs.lower())
223225

224226

225227
def lookup_in(
226-
data: t.Union[str, list[str], Mapping[str, str]],
227-
rhs: t.Union[str, list[str], Mapping[str, str], re.Pattern[str]],
228+
data: str | list[str] | Mapping[str, str],
229+
rhs: str | list[str] | Mapping[str, str] | re.Pattern[str],
228230
) -> bool:
229231
if isinstance(rhs, list):
230232
return data in rhs
@@ -248,8 +250,8 @@ def lookup_in(
248250

249251

250252
def lookup_nin(
251-
data: t.Union[str, list[str], Mapping[str, str]],
252-
rhs: t.Union[str, list[str], Mapping[str, str], re.Pattern[str]],
253+
data: str | list[str] | Mapping[str, str],
254+
rhs: str | list[str] | Mapping[str, str] | re.Pattern[str],
253255
) -> bool:
254256
if isinstance(rhs, list):
255257
return data not in rhs
@@ -273,17 +275,17 @@ def lookup_nin(
273275

274276

275277
def lookup_regex(
276-
data: t.Union[str, list[str], Mapping[str, str]],
277-
rhs: t.Union[str, list[str], Mapping[str, str], re.Pattern[str]],
278+
data: str | list[str] | Mapping[str, str],
279+
rhs: str | list[str] | Mapping[str, str] | re.Pattern[str],
278280
) -> bool:
279281
if isinstance(data, (str, bytes, re.Pattern)) and isinstance(rhs, (str, bytes)):
280282
return bool(re.search(rhs, data))
281283
return False
282284

283285

284286
def lookup_iregex(
285-
data: t.Union[str, list[str], Mapping[str, str]],
286-
rhs: t.Union[str, list[str], Mapping[str, str], re.Pattern[str]],
287+
data: str | list[str] | Mapping[str, str],
288+
rhs: str | list[str] | Mapping[str, str] | re.Pattern[str],
287289
) -> bool:
288290
if isinstance(data, (str, bytes, re.Pattern)) and isinstance(rhs, (str, bytes)):
289291
return bool(re.search(rhs, data, re.IGNORECASE))
@@ -467,9 +469,9 @@ class QueryList(list[T], t.Generic[T]):
467469
"""
468470

469471
data: Sequence[T]
470-
pk_key: t.Optional[str]
472+
pk_key: str | None
471473

472-
def __init__(self, items: t.Optional["Iterable[T]"] = None) -> None:
474+
def __init__(self, items: Iterable[T] | None = None) -> None:
473475
super().__init__(items if items is not None else [])
474476

475477
def items(self) -> list[tuple[str, T]]:
@@ -502,9 +504,9 @@ def __eq__(
502504

503505
def filter(
504506
self,
505-
matcher: t.Optional[t.Union[Callable[[T], bool], T]] = None,
507+
matcher: Callable[[T], bool] | T | None = None,
506508
**kwargs: t.Any,
507-
) -> "QueryList[T]":
509+
) -> QueryList[T]:
508510
def filter_lookup(obj: t.Any) -> bool:
509511
for path, v in kwargs.items():
510512
try:
@@ -529,7 +531,7 @@ def filter_lookup(obj: t.Any) -> bool:
529531
filter_ = matcher
530532
elif matcher is not None:
531533

532-
def val_match(obj: t.Union[str, list[t.Any], T]) -> bool:
534+
def val_match(obj: str | list[t.Any] | T) -> bool:
533535
if isinstance(matcher, list):
534536
return obj in matcher
535537
return bool(obj == matcher)
@@ -542,10 +544,10 @@ def val_match(obj: t.Union[str, list[t.Any], T]) -> bool:
542544

543545
def get(
544546
self,
545-
matcher: t.Optional[t.Union[Callable[[T], bool], T]] = None,
546-
default: t.Optional[t.Any] = no_arg,
547+
matcher: Callable[[T], bool] | T | None = None,
548+
default: t.Any | None = no_arg,
547549
**kwargs: t.Any,
548-
) -> t.Optional[T]:
550+
) -> T | None:
549551
objs = self.filter(matcher=matcher, **kwargs)
550552
if len(objs) > 1:
551553
raise MultipleObjectsReturned

0 commit comments

Comments
 (0)