Skip to content

Commit 67bc50f

Browse files
committed
feat: Use a typing proxy to avoid imports/backports
1 parent c83c04d commit 67bc50f

File tree

3 files changed

+43
-19
lines changed

3 files changed

+43
-19
lines changed

src/bids_validator/types/_context.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,21 @@
1313
load_schema_into_namespace(schema['meta']['context'], globals(), 'Context')
1414
"""
1515

16+
from __future__ import annotations
17+
1618
import json
17-
from typing import Any, Union
1819

1920
import attrs
2021
import bidsschematools as bst
2122
import bidsschematools.schema
2223

24+
from . import _typings as t
25+
2326
LATEST_SCHEMA_URL = 'https://bids-specification.readthedocs.io/en/latest/schema.json'
2427
STABLE_SCHEMA_URL = 'https://bids-specification.readthedocs.io/en/stable/schema.json'
2528

2629

27-
def get_schema(url: Union[str, None] = None) -> bst.types.Namespace:
30+
def get_schema(url: str | None = None) -> bst.types.Namespace:
2831
"""Load a BIDS schema from a URL or return the bundled schema if no URL is provided.
2932
3033
This function utilizes the ``universal_pathlib`` package to handle various URL schemes.
@@ -75,7 +78,7 @@ def snake_to_pascal(val: str) -> str:
7578
return ''.join(sub.capitalize() for sub in val.split('_'))
7679

7780

78-
def typespec_to_type(name: str, typespec: dict[str, Any]) -> tuple[type, dict[str, Any]]:
81+
def typespec_to_type(name: str, typespec: dict[str, t.Any]) -> tuple[type, dict[str, t.Any]]:
7982
"""Convert JSON-schema style specification to type and metadata dictionary."""
8083
tp = typespec.get('type')
8184
if not tp:
@@ -86,12 +89,12 @@ def typespec_to_type(name: str, typespec: dict[str, Any]) -> tuple[type, dict[st
8689
if properties:
8790
type_ = create_attrs_class(name, properties=properties, metadata=metadata)
8891
else:
89-
type_ = dict[str, Any]
92+
type_ = dict[str, t.Any]
9093
elif tp == 'array':
9194
if 'items' in typespec:
9295
subtype, md = typespec_to_type(name, typespec['items'])
9396
else:
94-
subtype = Any
97+
subtype = t.Any
9598
type_ = list[subtype]
9699
else:
97100
type_ = {
@@ -111,8 +114,8 @@ def _type_name(tp: type) -> str:
111114

112115
def create_attrs_class(
113116
class_name: str,
114-
properties: dict[str, Any],
115-
metadata: dict[str, Any],
117+
properties: dict[str, t.Any],
118+
metadata: dict[str, t.Any],
116119
) -> type:
117120
"""Dynamically create an attrs class with the given properties.
118121
@@ -160,7 +163,7 @@ def create_attrs_class(
160163

161164

162165
def generate_attrs_classes_from_schema(
163-
schema: dict[str, Any],
166+
schema: dict[str, t.Any],
164167
root_class_name: str,
165168
) -> type:
166169
"""Generate attrs classes from a JSON schema.
@@ -185,7 +188,7 @@ def generate_attrs_classes_from_schema(
185188
return type_
186189

187190

188-
def populate_namespace(attrs_class: type, namespace: dict[str, Any]) -> None:
191+
def populate_namespace(attrs_class: type, namespace: dict[str, t.Any]) -> None:
189192
"""Populate a namespace with nested attrs classes.
190193
191194
Parameters
@@ -205,8 +208,8 @@ def populate_namespace(attrs_class: type, namespace: dict[str, Any]) -> None:
205208

206209

207210
def load_schema_into_namespace(
208-
schema: dict[str, Any],
209-
namespace: dict[str, Any],
211+
schema: dict[str, t.Any],
212+
namespace: dict[str, t.Any],
210213
root_class_name: str,
211214
) -> None:
212215
"""Load a JSON schema into a namespace as attrs classes.

src/bids_validator/types/_typings.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
__all__ = (
2+
'Self',
3+
'TYPE_CHECKING',
4+
'Any',
5+
)
6+
7+
TYPE_CHECKING = False
8+
if TYPE_CHECKING:
9+
from typing import Any, Self
10+
else:
11+
12+
def __getattr__(name: str):
13+
if name in __all__:
14+
import typing
15+
16+
return getattr(typing, name)
17+
18+
msg = f'Module {__name__!r} has no attribute {name!r}'
19+
raise AttributeError(msg)

src/bids_validator/types/files.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
"""Types for working with file trees."""
22

3+
from __future__ import annotations
4+
35
import os
46
import posixpath
57
import stat
68
from functools import cached_property
79
from pathlib import Path
8-
from typing import Union
910

1011
import attrs
11-
from typing_extensions import Self # PY310
12+
13+
from . import _typings as t
1214

1315
__all__ = ('FileTree',)
1416

@@ -58,7 +60,7 @@ def is_symlink(self) -> bool:
5860
return stat.S_ISLNK(_stat.st_mode)
5961

6062

61-
def as_direntry(obj: os.PathLike) -> Union[os.DirEntry, UserDirEntry]:
63+
def as_direntry(obj: os.PathLike) -> os.DirEntry | UserDirEntry:
6264
"""Convert PathLike into DirEntry-like object."""
6365
if isinstance(obj, os.DirEntry):
6466
return obj
@@ -69,10 +71,10 @@ def as_direntry(obj: os.PathLike) -> Union[os.DirEntry, UserDirEntry]:
6971
class FileTree:
7072
"""Represent a FileTree with cached metadata."""
7173

72-
direntry: Union[os.DirEntry, UserDirEntry] = attrs.field(repr=False, converter=as_direntry)
73-
parent: Union['FileTree', None] = attrs.field(repr=False, default=None)
74+
direntry: os.DirEntry | UserDirEntry = attrs.field(repr=False, converter=as_direntry)
75+
parent: FileTree | None = attrs.field(repr=False, default=None)
7476
is_dir: bool = attrs.field(default=False)
75-
children: dict[str, 'FileTree'] = attrs.field(repr=False, factory=dict)
77+
children: dict[str, FileTree] = attrs.field(repr=False, factory=dict)
7678
name: str = attrs.field(init=False)
7779

7880
def __attrs_post_init__(self):
@@ -85,8 +87,8 @@ def __attrs_post_init__(self):
8587
def read_from_filesystem(
8688
cls,
8789
direntry: os.PathLike,
88-
parent: Union['FileTree', None] = None,
89-
) -> Self:
90+
parent: FileTree | None = None,
91+
) -> t.Self:
9092
"""Read a FileTree from the filesystem.
9193
9294
Uses :func:`os.scandir` to walk the directory tree.

0 commit comments

Comments
 (0)