diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index b6588a4..e74e23d 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.10', '3.11', '3.12', '3.13'] fail-fast: false steps: - uses: actions/checkout@v6 @@ -69,4 +69,4 @@ jobs: run: poetry run mypy - name: Upload coverage - uses: codecov/codecov-action@v5 \ No newline at end of file + uses: codecov/codecov-action@v5 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 786c864..eff795e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ --- -default_stages: [commit, push] +default_stages: [pre-commit, pre-push] default_language_version: # force all unspecified python hooks to run python3 python: python3 diff --git a/AGENTS.md b/AGENTS.md index 429ee33..d02f4e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This file is a concise, repo-specific guide for agentic coding tools. If anything here conflicts with code or CI, follow the code/CI. ## Quick facts -- Primary language: Python (>=3.9) +- Primary language: Python (>=3.10) - Build/packaging: Poetry - Tests: pytest + coverage - Lint/format: black, isort, flynt (pre-commit) @@ -93,4 +93,4 @@ If anything here conflicts with code or CI, follow the code/CI. ## Notes for agents - Prefer editing minimal sections; do not reformat unrelated code. - Run targeted tests when possible (single file or single test). -- Keep changes compatible with Python 3.9+. +- Keep changes compatible with Python 3.10+. diff --git a/pathable/accessors.py b/pathable/accessors.py index 8d6b269..d93236b 100644 --- a/pathable/accessors.py +++ b/pathable/accessors.py @@ -1,7 +1,6 @@ """Pathable accessors module""" import stat -import sys from collections import OrderedDict from collections.abc import Hashable from collections.abc import Mapping @@ -9,9 +8,7 @@ from pathlib import Path from typing import Any from typing import Generic -from typing import Optional from typing import TypeVar -from typing import Union from pathable.protocols import Subscriptable from pathable.types import LookupKey @@ -45,7 +42,7 @@ def __eq__(self, other: object) -> Any: return NotImplemented return self.node == other.node - def stat(self, parts: Sequence[K]) -> Union[dict[str, Any], None]: + def stat(self, parts: Sequence[K]) -> dict[str, Any] | None: raise NotImplementedError def keys(self, parts: Sequence[K]) -> Sequence[K]: @@ -156,8 +153,9 @@ def _is_traversable_node(cls, node: N) -> bool: @classmethod def _get_node(cls, node: N, parts: Sequence[K]) -> N: current = node + get_subnode = cls._get_subnode for part in parts: - current = cls._get_subnode(current, part) + current = get_subnode(current, part) return current @classmethod @@ -171,14 +169,10 @@ def _get_subnode(cls, node: N, part: K) -> N: class PathAccessor(NodeAccessor[Path, str, bytes]): - def stat(self, parts: Sequence[str]) -> Union[dict[str, Any], None]: + def stat(self, parts: Sequence[str]) -> dict[str, Any] | None: subpath = self.node.joinpath(*parts) try: - # Avoid following symlinks (Python 3.10+) - if sys.version_info >= (3, 10): - stat = subpath.stat(follow_symlinks=False) - else: - stat = subpath.lstat() + stat = subpath.stat(follow_symlinks=False) except OSError: return None return { @@ -252,14 +246,14 @@ def _get_subnode(cls, node: Path, part: str) -> Path: class SubscriptableAccessor( - NodeAccessor[Union[Subscriptable[SK, SV], SV], SK, SV], Generic[SK, SV] + NodeAccessor[Subscriptable[SK, SV] | SV, SK, SV], Generic[SK, SV] ): """Accessor for subscriptable content.""" @classmethod def _get_subnode( - cls, node: Union[Subscriptable[SK, SV], SV], part: SK - ) -> Union[Subscriptable[SK, SV], SV]: + cls, node: Subscriptable[SK, SV] | SV, part: SK + ) -> Subscriptable[SK, SV] | SV: if not isinstance(node, Subscriptable): raise KeyError(part) try: @@ -271,13 +265,13 @@ def _get_subnode( class CachedSubscriptableAccessor( SubscriptableAccessor[CSK, CSV], Generic[CSK, CSV] ): - def __init__(self, node: Union[Subscriptable[CSK, CSV], CSV]): + def __init__(self, node: Subscriptable[CSK, CSV] | CSV): super().__init__(node) # Per-instance cache: avoids global strong references and id-reuse hazards. # Default maxsize matches functools.lru_cache default (128). self._cache_enabled = True - self._cache_maxsize: Optional[int] = 128 + self._cache_maxsize: int | None = 128 self._cache: OrderedDict[tuple[CSK, ...], CSV] = OrderedDict() def clear_cache(self) -> None: @@ -289,7 +283,7 @@ def disable_cache(self) -> None: self._cache_enabled = False self._cache.clear() - def enable_cache(self, *, maxsize: Optional[int] = 128) -> None: + def enable_cache(self, *, maxsize: int | None = 128) -> None: """Enable caching for this accessor instance. Args: @@ -331,23 +325,23 @@ class LookupAccessor(CachedSubscriptableAccessor[LookupKey, LookupValue]): @classmethod def _is_traversable_node(cls, node: LookupNode) -> bool: - return isinstance(node, Mapping) or isinstance(node, list) + return isinstance(node, Mapping | list) - def stat(self, parts: Sequence[LookupKey]) -> Union[dict[str, Any], None]: + def stat(self, parts: Sequence[LookupKey]) -> dict[str, Any] | None: try: node = self[parts] except KeyError: return None - if self._is_traversable_node(node): - return { - "type": type(node).__name__, - "length": len(node), - } - try: - length = len(node) - except TypeError: - length = None + length: int | None + match node: + case Mapping() | list(): + length = len(node) + case _: + try: + length = len(node) + except TypeError: + length = None return { "type": type(node).__name__, @@ -360,13 +354,13 @@ def contains(self, parts: Sequence[LookupKey], key: LookupKey) -> bool: except KeyError: return False - if isinstance(node, Mapping): - return key in node - - if isinstance(node, list): - return isinstance(key, int) and 0 <= key < len(node) - - return False + match node: + case Mapping(): + return key in node + case list() as items: + return isinstance(key, int) and 0 <= key < len(items) + case _: + return False def require_child( self, parts: Sequence[LookupKey], key: LookupKey @@ -374,24 +368,25 @@ def require_child( # Validate parent path for intermediate diagnostics. node = self[parts] - if isinstance(node, Mapping): - if key not in node: - raise KeyError(key) - return - - if isinstance(node, list): - if not (isinstance(key, int) and 0 <= key < len(node)): + match node: + case Mapping(): + if key not in node: + raise KeyError(key) + return + case list() as items: + if not (isinstance(key, int) and 0 <= key < len(items)): + raise KeyError(key) + return + case _: raise KeyError(key) - return - - raise KeyError(key) def keys(self, parts: Sequence[LookupKey]) -> Sequence[LookupKey]: node = self[parts] - if isinstance(node, Mapping): - return list(node.keys()) - if isinstance(node, list): - return list(range(len(node))) + match node: + case Mapping(): + return list(node.keys()) + case list() as items: + return list(range(len(items))) # Non-traversable leaf. if parts: raise KeyError(parts[-1]) diff --git a/pathable/parsers.py b/pathable/parsers.py index 6f67ede..1aa5102 100644 --- a/pathable/parsers.py +++ b/pathable/parsers.py @@ -1,7 +1,5 @@ """Pathable parsers module""" -from __future__ import annotations - from collections.abc import Hashable from typing import Sequence @@ -12,6 +10,17 @@ def parse_parts( parts: Sequence[Hashable | None], sep: str = SEPARATOR ) -> list[Hashable]: """Parse (filter and split) path parts.""" + + def append_split(part: str) -> None: + if not part or part == ".": + return + if sep_check in part: + for split_part in reversed(part.split(sep_check)): + if split_part and split_part != ".": + append(split_part) + return + append(part) + parsed: list[Hashable] = [] append = parsed.append sep_check = sep @@ -24,24 +33,11 @@ def parse_parts( continue # Fast-path: str is most common. if isinstance(part, str): - if part and part != ".": - if sep_check in part: - for x in reversed(part.split(sep_check)): - if x and x != ".": - append(x) - else: - append(part) + append_split(part) continue # Fast-path: bytes, decode then treat as str. if isinstance(part, bytes): - part = part.decode("ascii") - if part and part != ".": - if sep_check in part: - for x in reversed(part.split(sep_check)): - if x and x != ".": - append(x) - else: - append(part) + append_split(part.decode("ascii")) continue # Fallback: Hashable (covers e.g. tuple, custom keys). if isinstance(part, Hashable): diff --git a/pathable/paths.py b/pathable/paths.py index 4b8a3da..ee1ca2c 100644 --- a/pathable/paths.py +++ b/pathable/paths.py @@ -9,11 +9,8 @@ from pathlib import Path from typing import Any from typing import Generic -from typing import Optional from typing import Sequence -from typing import Type from typing import TypeVar -from typing import Union from typing import cast from typing import overload @@ -42,7 +39,7 @@ class BasePath: parts: tuple[Hashable, ...] separator: str = SEPARATOR - def __init__(self, *args: Any, separator: Optional[str] = None): + def __init__(self, *args: Any, separator: str | None = None): object.__setattr__(self, "separator", separator or self.separator) parts = self._parse_args(args, sep=self.separator) object.__setattr__(self, "parts", parts) @@ -60,42 +57,53 @@ def _parse_args( behavior. """ parts: list[Hashable] = [] + append = parts.append + extend = parts.extend - for a in args: - if isinstance(a, cls): - parts.extend(a.parts) + for arg in args: + part: Any = arg + + if isinstance(part, cls): + extend(part.parts) continue - if isinstance(a, bytes): - a = a.decode("ascii") - if isinstance(a, os.PathLike): - a = os.fspath(a) - if isinstance(a, bytes): - a = a.decode("ascii") - if isinstance(a, (str, int)): - parts.append(a) + + if isinstance(part, bytes): + append(part.decode("ascii")) continue - if isinstance(a, Hashable): - parts.append(a) + + if isinstance(part, os.PathLike): + part = os.fspath(part) + if isinstance(part, bytes): + append(part.decode("ascii")) + continue + + if isinstance(part, (str, int)): + append(part) continue + + if isinstance(part, Hashable): + append(part) + continue + raise TypeError( "argument must be Hashable, bytes, os.PathLike, or BasePath; got %r" - % (type(a),) + % (type(part),) ) return tuple(parse_parts(parts, sep)) @classmethod def _from_parts( - cls: Type[TBasePath], + cls: type[TBasePath], args: Sequence[Any], - separator: Optional[str] = None, + separator: str | None = None, ) -> TBasePath: return cls(*args, separator=separator) @classmethod def _from_parsed_parts( - cls: Type[TBasePath], + cls: type[TBasePath], parts: tuple[Hashable, ...], - separator: Optional[str] = None, + separator: str | None = None, ) -> "TBasePath": instance = cls.__new__(cls) object.__setattr__(instance, "parts", parts) @@ -119,7 +127,7 @@ def _cmp_parts(self) -> tuple[tuple[str, str], ...]: """ return tuple( (f"{type(p).__module__}.{type(p).__qualname__}", c) - for p, c in zip(self.parts, self._cparts) + for p, c in zip(self.parts, self._cparts, strict=True) ) def _make_child(self: TBasePath, args: list[Any]) -> TBasePath: @@ -340,17 +348,17 @@ def __init__( self, accessor: NodeAccessor[N, K, V], *args: Any, - separator: Optional[str] = None, + separator: str | None = None, ): object.__setattr__(self, "accessor", accessor) super().__init__(*args, separator=separator) @classmethod def _from_parts( - cls: Type[TAccessorPath], + cls: type[TAccessorPath], args: Sequence[Any], - separator: Optional[str] = None, - accessor: Union[NodeAccessor[N, K, V], None] = None, + separator: str | None = None, + accessor: NodeAccessor[N, K, V] | None = None, ) -> TAccessorPath: if accessor is None: raise ValueError("accessor must be provided") @@ -358,10 +366,10 @@ def _from_parts( @classmethod def _from_parsed_parts( - cls: Type[TAccessorPath], + cls: type[TAccessorPath], parts: tuple[Hashable, ...], - separator: Optional[str] = None, - accessor: Union[NodeAccessor[N, K, V], None] = None, + separator: str | None = None, + accessor: NodeAccessor[N, K, V] | None = None, ) -> TAccessorPath: if accessor is None: raise ValueError("accessor must be provided") @@ -396,7 +404,7 @@ def __rtruediv__(self: TAccessorPath, key: Hashable) -> TAccessorPath: def __floordiv__(self: TAccessorPath, key: K) -> TAccessorPath: """Return a new existing path with the key appended.""" self.accessor.require_child(self.parts, key) - return self / key + return self._make_child_relpath(key) def __rfloordiv__(self: TAccessorPath, key: K) -> TAccessorPath: """Return a new existing path with the key prepended.""" @@ -419,7 +427,7 @@ def __iter__(self: TAccessorPath) -> Iterator[TAccessorPath]: for key in self.accessor.keys(self.parts): yield self._make_child_relpath(key) - def __getitem__(self: TAccessorPath, key: K) -> Union[V, TAccessorPath]: + def __getitem__(self: TAccessorPath, key: K) -> V | TAccessorPath: """Access a child path's value.""" path = self // key if path.is_traversable(): @@ -466,10 +474,10 @@ def items(self: TAccessorPath) -> Iterator[tuple[K, TAccessorPath]]: yield key, self._make_child_relpath(key) @overload - def get(self, key: K) -> Optional[V]: ... + def get(self, key: K) -> V | None: ... @overload - def get(self, key: K, default: TDefault) -> Union[V, TDefault]: ... + def get(self, key: K, default: TDefault) -> V | TDefault: ... def get(self, key: K, default: object = None) -> object: """Return the value for key if key is in the path, else default.""" @@ -482,7 +490,7 @@ def read_value(self) -> V: """Return the path's value.""" return self.accessor.read(self.parts) - def stat(self) -> Union[dict[str, Any], None]: + def stat(self) -> dict[str, Any] | None: """Return metadata for the path, or None if it doesn't exist.""" return self.accessor.stat(self.parts) @@ -500,7 +508,7 @@ class FilesystemPath(AccessorPath[Path, str, bytes]): @classmethod def from_path( - cls: Type["FilesystemPath"], + cls: type["FilesystemPath"], path: Path, ) -> "FilesystemPath": """Public constructor for a Path-backed path.""" diff --git a/pathable/types.py b/pathable/types.py index 262dd6b..6fbf72c 100644 --- a/pathable/types.py +++ b/pathable/types.py @@ -1,10 +1,9 @@ """Pathable types module""" from typing import Any -from typing import Union from pathable.protocols import Subscriptable -LookupKey = Union[str, int] +LookupKey = str | int LookupValue = Any -LookupNode = Union[Subscriptable[LookupKey, LookupValue], LookupValue] +LookupNode = Subscriptable[LookupKey, LookupValue] | LookupValue diff --git a/poetry.lock b/poetry.lock index e0695b0..23d7ac6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -258,7 +258,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -319,31 +319,6 @@ files = [ [package.extras] license = ["ukkonen"] -[[package]] -name = "importlib-metadata" -version = "8.7.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, - {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -perf = ["ipython"] -test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] - [[package]] name = "iniconfig" version = "1.1.1" @@ -368,9 +343,6 @@ files = [ {file = "isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481"}, ] -[package.dependencies] -importlib-metadata = {version = ">=4.6.0", markers = "python_version < \"3.10\""} - [package.extras] colors = ["colorama"] plugins = ["setuptools"] @@ -964,28 +936,7 @@ six = ">=1.9.0,<2" docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0) ; python_version > \"3.4\"", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [metadata] lock-version = "2.1" -python-versions = ">=3.9,<4.0" -content-hash = "5c1267903ccd39069eeb7a75149092fdc10de8d37e0d25f8cb3169c34015767a" +python-versions = ">=3.10,<4.0" +content-hash = "fef5732c8b443ab4a7d7f997abb08cc050886e42b3e5c882df3f5b3ecf3e7314" diff --git a/pyproject.toml b/pyproject.toml index d3db40d..ef91984 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,6 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -57,7 +56,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = ">=3.9,<4.0" +python = ">=3.10,<4.0" [tool.poetry.group.dev.dependencies] tbump = "^6.11.0" diff --git a/tests/benchmarks/bench_utils.py b/tests/benchmarks/bench_utils.py index e20400e..5942187 100644 --- a/tests/benchmarks/bench_utils.py +++ b/tests/benchmarks/bench_utils.py @@ -17,7 +17,6 @@ from typing import Iterable from typing import Mapping from typing import MutableMapping -from typing import Optional @dataclass(frozen=True) @@ -46,7 +45,7 @@ def ops_per_sec_median(self) -> float: return 1.0 / per -def _safe_int_env(name: str) -> Optional[int]: +def _safe_int_env(name: str) -> int | None: value = os.environ.get(name) if value is None: return None @@ -110,7 +109,7 @@ def run_benchmark( def results_to_json( *, results: Iterable[BenchmarkResult], - meta: Optional[Mapping[str, Any]] = None, + meta: Mapping[str, Any] | None = None, ) -> dict[str, Any]: out: dict[str, Any] = { "meta": dict(meta or default_meta()), diff --git a/tests/unit/test_filesystem.py b/tests/unit/test_filesystem.py index 4d6e08e..79f7998 100644 --- a/tests/unit/test_filesystem.py +++ b/tests/unit/test_filesystem.py @@ -1,7 +1,6 @@ """Tests for PathAccessor and FilesystemPath.""" import os -import sys from pathlib import Path from unittest.mock import Mock from unittest.mock import patch @@ -77,12 +76,8 @@ def test_stat_nested_path(self, tmp_path): assert result is not None assert result["st_size"] == 6 # "nested" is 6 bytes - @pytest.mark.skipif( - sys.version_info < (3, 10), - reason="follow_symlinks requires Python 3.10+", - ) - def test_stat_symlink_not_followed_py310(self, tmp_path): - """Test stat does not follow symlinks on Python 3.10+.""" + def test_stat_symlink_not_followed(self, tmp_path): + """Test stat does not follow symlinks.""" target_file = tmp_path / "target.txt" target_file.write_text("target content") @@ -93,7 +88,7 @@ def test_stat_symlink_not_followed_py310(self, tmp_path): result = accessor.stat(["link.txt"]) assert result is not None - # On 3.10+, should use stat(follow_symlinks=False) + # Should use stat(follow_symlinks=False) # which means we get the symlink's own stat, not the target's # Verify it's the symlink by checking the size doesn't match target target_stat = target_file.stat() @@ -102,29 +97,6 @@ def test_stat_symlink_not_followed_py310(self, tmp_path): # For symlinks, size should be small (path length), not target size assert result["st_size"] != target_stat.st_size - @pytest.mark.skipif( - sys.version_info >= (3, 10), reason="lstat used on Python < 3.10" - ) - def test_stat_uses_lstat_pre_py310(self, tmp_path): - """Test stat uses lstat on Python < 3.10.""" - target_file = tmp_path / "target.txt" - target_file.write_text("target content") - - link_file = tmp_path / "link.txt" - link_file.symlink_to(target_file) - - accessor = PathAccessor(tmp_path) - result = accessor.stat(["link.txt"]) - - assert result is not None - # On <3.10, should use lstat() - # Verify it's the symlink by checking the size doesn't match target - target_stat = target_file.stat() - link_stat = link_file.lstat() - assert result["st_size"] == link_stat.st_size - # For symlinks, size should be small (path length), not target size - assert result["st_size"] != target_stat.st_size - class TestPathAccessorKeys: """Tests for PathAccessor.keys() method.""" diff --git a/tests/unit/test_paths.py b/tests/unit/test_paths.py index e0822f8..127477a 100644 --- a/tests/unit/test_paths.py +++ b/tests/unit/test_paths.py @@ -1,9 +1,9 @@ from collections.abc import Hashable from collections.abc import Mapping +from collections.abc import Sequence from pathlib import Path from types import GeneratorType from typing import Any -from typing import Union from uuid import uuid4 import pytest @@ -17,9 +17,7 @@ from pathable.paths import LookupPath -class MockAccessor( - NodeAccessor[Union[Mapping[Hashable, Any], Any], Hashable, Any] -): +class MockAccessor(NodeAccessor[Mapping[Hashable, Any] | Any, Hashable, Any]): """Mock accessor.""" def __init__( @@ -30,15 +28,13 @@ def __init__( self._content = content self._exists = exists - def keys(self, parts: list[Hashable]) -> Any: + def keys(self, parts: Sequence[Hashable]) -> Any: return self._children_keys - def len(self, parts: list[Hashable]) -> int: + def len(self, parts: Sequence[Hashable]) -> int: return len(self._children_keys) - def read( - self, parts: list[Hashable] - ) -> Union[Mapping[Hashable, Any], Any]: + def read(self, parts: Sequence[Hashable]) -> Mapping[Hashable, Any] | Any: return self._content @@ -50,17 +46,17 @@ class MockTraversableAccessor( def __init__(self, node: Mapping[Hashable, Any]): super().__init__(node) - def keys(self, parts: list[Hashable]) -> Any: + def keys(self, parts: Sequence[Hashable]) -> Any: node = self._get_node(self.node, parts) if isinstance(node, Mapping): return list(node.keys()) raise KeyError - def len(self, parts: list[Hashable]) -> int: + def len(self, parts: Sequence[Hashable]) -> int: keys = self.keys(parts) return len(keys) - def read(self, parts: list[Hashable]) -> Any: + def read(self, parts: Sequence[Hashable]) -> Any: return self._read_node(self._get_node(self.node, parts)) @classmethod @@ -833,7 +829,7 @@ def test_value(self): class TestAccessorPathContains: class KeysKeyErrorAccessor(MockAccessor): - def keys(self, parts: list[Hashable]) -> Any: + def keys(self, parts: Sequence[Hashable]) -> Any: raise KeyError def test_valid(self): diff --git a/tests/unit/test_traversable.py b/tests/unit/test_traversable.py index cf9277b..1a0c984 100644 --- a/tests/unit/test_traversable.py +++ b/tests/unit/test_traversable.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from typing import Any from typing import Sequence