pathlib-next 0.8.0__tar.gz → 0.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/CHANGELOG.md +41 -1
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/PKG-INFO +1 -1
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/pyproject.toml +1 -1
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/fspath.py +31 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/utils/__init__.py +19 -2
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_smoke.py +7 -2
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_utils.py +61 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/.gitignore +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/LICENSE +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/README.md +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/docs/api/mempath.md +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/docs/api/path.md +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/docs/api/testing.md +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/docs/api/uri.md +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/docs/api/utils.md +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/docs/benchmarks.md +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/docs/changelog.md +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/docs/divergences.md +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/docs/guides/cli.md +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/docs/guides/extending.md +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/docs/guides/schemes.md +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/docs/index.md +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/examples/az_listing.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/examples/data_and_archive.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/examples/ftp_listing.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/examples/github_listing.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/examples/gitlab_listing.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/examples/gs_listing.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/examples/http_listing.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/examples/local_and_mem.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/examples/s3_listing.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/examples/sftp_sync.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/examples/webdav_roundtrip.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/mkdocs.yml +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/__init__.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/mempath.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/path.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/protocols/__init__.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/protocols/fs.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/protocols/io.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/py.typed +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/testing.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/tools/__init__.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/tools/uripath.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/__init__.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/query.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/__init__.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/_gitrepo.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/archive/__init__.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/archive/_base.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/archive/tar.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/archive/zip.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/az.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/data.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/dav.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/file.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/ftp.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/git/__init__.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/git/_base.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/git/github.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/git/gitlab.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/github.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/gitlab.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/gs.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/http.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/s3.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/sftp/__init__.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/sftp/_asyncssh.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/schemes/sftp/_paramiko.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/uri/source.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/utils/archive.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/utils/checksum.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/utils/glob.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/utils/stat.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/src/pathlib_next/utils/sync.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/conftest.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_archive_uri.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_az.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_az_fake.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_contract.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_data_uri.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_dav.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_ftp.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_gitrepo.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_glob.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_gs.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_gs_fake.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_http.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_http_live.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_http_parser.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_local.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_mempath.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_parity_io.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_parity_pure.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_path_gaps.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_pathname.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_plugins.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_properties.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_query.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_s3.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_sftp.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_sftp_asyncssh.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_source.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_sync.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_uri_parse.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_uri_path.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_uripath_tool.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_walk.py +0 -0
- {pathlib_next-0.8.0 → pathlib_next-0.8.1}/tests/test_webdav.py +0 -0
|
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.1] - 2026-07-16
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`LocalPath.walk()`/`rm()` raised `TypeError: cannot unpack non-iterable
|
|
14
|
+
DirEntry object` on Python 3.11/3.12.** Those stdlib versions define their
|
|
15
|
+
own `pathlib.Path._scandir()` (returning raw `os.scandir()` `DirEntry`
|
|
16
|
+
objects), which sits ahead of this project's `_scandir()` in `LocalPath`'s
|
|
17
|
+
MRO and silently shadowed it -- breaking the `(name, FileStat|None)`
|
|
18
|
+
contract `walk()`/`glob()`/`rm()` expect. `LocalPath` now defines its own
|
|
19
|
+
`_scandir()` explicitly, reusing each `DirEntry`'s cached `lstat()` so the
|
|
20
|
+
perf win from `_scandir()` unification is preserved. On 3.12+, stdlib
|
|
21
|
+
`pathlib.Path` also defines its own `walk()` ahead of ours in the MRO, and
|
|
22
|
+
that stdlib `walk()` treats `self._scandir()`'s return value as a context
|
|
23
|
+
manager (`with scandir_it:`) -- our own `_scandir()` is a plain generator,
|
|
24
|
+
so stdlib's `walk()` raised `TypeError: 'generator' object does not
|
|
25
|
+
support the context manager protocol` even with the override above.
|
|
26
|
+
`LocalPath` now also overrides `walk()` explicitly, routing to this
|
|
27
|
+
project's own implementation regardless of Python version. Introduced in
|
|
28
|
+
0.8.0 (`8cdbefa`), exposed on the CI 3.11/3.12 legs.
|
|
29
|
+
- **`Test No-Extras` CI job was red.** `tests/test_smoke.py` unconditionally
|
|
30
|
+
constructed an `http://`/`sftp://` `UriPath` in two tests, requiring
|
|
31
|
+
`requests`/`paramiko` even though the no-extras job installs neither; a
|
|
32
|
+
third test wrongly assumed `S3Path` requires `boto3` to register (it only
|
|
33
|
+
needs `botocore`, imported lazily inside a method). The two hard tests now
|
|
34
|
+
`pytest.importorskip` their extra; the `S3Path` check now probes for
|
|
35
|
+
`botocore`. Introduced in 0.8.0 (`94bd545`/`8cdbefa`), fixed with the
|
|
36
|
+
expected skip count (2) verified in a real no-extras venv.
|
|
37
|
+
- **Importable on a clean Python 3.9 install.** `pathlib_next.utils` used
|
|
38
|
+
`typing.ParamSpec` (3.10+), falling back to `typing_extensions.ParamSpec` and
|
|
39
|
+
then to a bare `typing.TypeVar`. A `TypeVar` has no `.args`, so the
|
|
40
|
+
`*args: K.args` annotations raised `AttributeError: 'TypeVar' object has no
|
|
41
|
+
attribute 'args'` at import time, making `import pathlib_next` fail on 3.9
|
|
42
|
+
whenever `typing_extensions` was absent. Since `typing_extensions` is not a
|
|
43
|
+
runtime dependency, this broke a plain `pip install pathlib_next` on 3.9. The
|
|
44
|
+
final fallback is now a minimal `ParamSpec` shim providing `.args`/`.kwargs`,
|
|
45
|
+
so no runtime dependency is added and 3.10+ keeps using `typing.ParamSpec`
|
|
46
|
+
unchanged.
|
|
47
|
+
|
|
10
48
|
## [0.8.0] - 2026-07-13
|
|
11
49
|
|
|
12
50
|
### Added
|
|
@@ -437,7 +475,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
437
475
|
- Sync error handling.
|
|
438
476
|
- Generic Path Protocol based pathlib implementation for URI paths with file access support for sftp, http, file schemes.
|
|
439
477
|
|
|
440
|
-
[Unreleased]: https://github.com/jose-pr/pathlib_next/compare/v0.
|
|
478
|
+
[Unreleased]: https://github.com/jose-pr/pathlib_next/compare/v0.8.1...HEAD
|
|
479
|
+
[0.8.1]: https://github.com/jose-pr/pathlib_next/compare/v0.8.0...v0.8.1
|
|
480
|
+
[0.8.0]: https://github.com/jose-pr/pathlib_next/compare/v0.7.0...v0.8.0
|
|
441
481
|
[0.7.0]: https://github.com/jose-pr/pathlib_next/compare/v0.6.0...v0.7.0
|
|
442
482
|
[0.6.0]: https://github.com/jose-pr/pathlib_next/compare/v0.5.0...v0.6.0
|
|
443
483
|
[0.5.0]: https://github.com/jose-pr/pathlib_next/compare/v0.4.1...v0.5.0
|
|
@@ -11,6 +11,7 @@ import types as _types
|
|
|
11
11
|
import typing as _ty
|
|
12
12
|
|
|
13
13
|
from . import path as _proto
|
|
14
|
+
from .utils.stat import FileStat as _FileStat
|
|
14
15
|
|
|
15
16
|
# pathlib.Path.stat()/chmod() only accept follow_symlinks= on 3.10+; below
|
|
16
17
|
# that, LocalPath (which inherits them directly from pathlib.Path via MRO,
|
|
@@ -84,6 +85,36 @@ class LocalPath(
|
|
|
84
85
|
|
|
85
86
|
__slots__ = ()
|
|
86
87
|
|
|
88
|
+
def _scandir(self):
|
|
89
|
+
# On 3.11+, `pathlib.Path._scandir()` (stdlib, ahead of ours in the
|
|
90
|
+
# MRO via WindowsPath/PosixPath) shadows `_proto.Path._scandir()`
|
|
91
|
+
# and returns `os.scandir(self)` directly -- an iterator of raw
|
|
92
|
+
# `os.DirEntry`, not this project's `(name, FileStat|None)` tuples.
|
|
93
|
+
# walk()/glob() expect the latter, so re-assert our own contract
|
|
94
|
+
# here regardless of what stdlib does in a given version. DirEntry's
|
|
95
|
+
# own cached lstat (`follow_symlinks=False`, matching walk()'s
|
|
96
|
+
# default) is reused instead of a fresh stat() round trip.
|
|
97
|
+
for entry in _os.scandir(self):
|
|
98
|
+
try:
|
|
99
|
+
stat = _FileStat.from_stat(entry.stat(follow_symlinks=False))
|
|
100
|
+
except OSError:
|
|
101
|
+
stat = None
|
|
102
|
+
yield entry.name, stat
|
|
103
|
+
|
|
104
|
+
def walk(self, top_down=True, on_error=None, follow_symlinks=False):
|
|
105
|
+
# 3.12+ stdlib `pathlib.Path.walk()` sits ahead of ours in the MRO
|
|
106
|
+
# and would otherwise win here. Its implementation calls
|
|
107
|
+
# `self._scandir()` expecting stdlib's own context-manager-capable
|
|
108
|
+
# `os.scandir(self)` return value ("with scandir_it:") -- our
|
|
109
|
+
# `_scandir()` override above is a plain generator, so stdlib's
|
|
110
|
+
# `walk()` breaks on it (`TypeError: 'generator' object does not
|
|
111
|
+
# support the context manager protocol`). Route explicitly to our
|
|
112
|
+
# own `walk()` (which drives `_scandir()` correctly) regardless of
|
|
113
|
+
# which one the MRO would otherwise resolve to.
|
|
114
|
+
return _proto.Path.walk(
|
|
115
|
+
self, top_down=top_down, on_error=on_error, follow_symlinks=follow_symlinks
|
|
116
|
+
)
|
|
117
|
+
|
|
87
118
|
def stat(self, *, follow_symlinks=True):
|
|
88
119
|
# pathlib.Path.stat() (next in MRO via WindowsPath/PosixPath) only
|
|
89
120
|
# accepts follow_symlinks= on 3.10+; below that, lstat() is the
|
|
@@ -6,12 +6,29 @@ from email.utils import parsedate as _parsedate
|
|
|
6
6
|
from threading import RLock
|
|
7
7
|
|
|
8
8
|
try:
|
|
9
|
-
ParamSpec = _ty.ParamSpec
|
|
9
|
+
ParamSpec = _ty.ParamSpec # 3.10+
|
|
10
10
|
except AttributeError:
|
|
11
11
|
try:
|
|
12
12
|
from typing_extensions import ParamSpec
|
|
13
13
|
except ImportError:
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
class ParamSpec(_ty.TypeVar, _root=True):
|
|
16
|
+
"""Minimal `ParamSpec` stand-in for 3.9 without `typing_extensions`.
|
|
17
|
+
|
|
18
|
+
Only the `.args` / `.kwargs` attributes are needed: they appear in
|
|
19
|
+
annotations that must merely *evaluate*, and a plain `TypeVar` has
|
|
20
|
+
neither. `typing_extensions` is not a runtime dependency, so the
|
|
21
|
+
fallback keeps a bare 3.9 install importable.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def args(self):
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def kwargs(self):
|
|
30
|
+
return self
|
|
31
|
+
|
|
15
32
|
|
|
16
33
|
K = ParamSpec("K")
|
|
17
34
|
V = _ty.TypeVar("V")
|
|
@@ -4,6 +4,8 @@ These are the snippets from README.md's Quick start and examples/example.py
|
|
|
4
4
|
that don't touch the network. On Python 3.9 this file is expected to fail
|
|
5
5
|
until the Python 3.9 compatibility work lands.
|
|
6
6
|
"""
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
7
9
|
import pathlib_next
|
|
8
10
|
from pathlib_next import Path, glob
|
|
9
11
|
from pathlib_next.mempath import MemPath
|
|
@@ -27,6 +29,7 @@ def test_readme_local_path():
|
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
def test_readme_http_path_construct_only():
|
|
32
|
+
pytest.importorskip("requests")
|
|
30
33
|
http_path = UriPath("http://example.com/data.txt")
|
|
31
34
|
assert http_path.source.scheme == "http"
|
|
32
35
|
|
|
@@ -80,6 +83,7 @@ def test_example_uripath_norm():
|
|
|
80
83
|
|
|
81
84
|
|
|
82
85
|
def test_example_uripath_sftp_join():
|
|
86
|
+
pytest.importorskip("paramiko")
|
|
83
87
|
sftp_root = UriPath("sftp://root@sftpexample/")
|
|
84
88
|
sftp_root.as_posix()
|
|
85
89
|
authkeys = sftp_root / "root/.ssh/authorized_keys"
|
|
@@ -111,9 +115,10 @@ def test_optional_schemes_presence_or_absence():
|
|
|
111
115
|
except ImportError:
|
|
112
116
|
assert not hasattr(schemes, "SftpPath")
|
|
113
117
|
|
|
114
|
-
# Check s3
|
|
118
|
+
# Check s3 -- S3Path only needs botocore at import time (boto3 itself is
|
|
119
|
+
# a lazy import inside S3Backend.client()), so that's what gates it.
|
|
115
120
|
try:
|
|
116
|
-
import
|
|
121
|
+
import botocore # noqa: F401
|
|
117
122
|
assert hasattr(schemes, "S3Path")
|
|
118
123
|
except ImportError:
|
|
119
124
|
assert not hasattr(schemes, "S3Path")
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import textwrap
|
|
5
|
+
|
|
1
6
|
import pytest
|
|
2
7
|
|
|
3
8
|
from pathlib_next import utils
|
|
@@ -306,3 +311,59 @@ def test_detect_format_does_not_peek_when_extension_is_conclusive():
|
|
|
306
311
|
assert _detect_format("a.tar", _boom) == "tar"
|
|
307
312
|
|
|
308
313
|
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@pytest.mark.skipif(
|
|
317
|
+
sys.version_info >= (3, 10),
|
|
318
|
+
reason="the ParamSpec fallback is only reachable on 3.9 (3.10+ has typing.ParamSpec)",
|
|
319
|
+
)
|
|
320
|
+
def test_paramspec_fallback_importable_without_typing_extensions():
|
|
321
|
+
# Regression: on Python 3.9 `typing.ParamSpec` does not exist, so the module
|
|
322
|
+
# falls back to `typing_extensions.ParamSpec` and, failing that, to a local
|
|
323
|
+
# shim. The old fallback was a bare `TypeVar`, which has no `.args`, so the
|
|
324
|
+
# `*args: K.args` annotations raised `AttributeError: 'TypeVar' object has no
|
|
325
|
+
# attribute 'args'` at import. It went unnoticed because every dev/CI env had
|
|
326
|
+
# `typing_extensions` installed transitively -- but it is not a runtime
|
|
327
|
+
# dependency, so a clean 3.9 install of the package could not import it.
|
|
328
|
+
#
|
|
329
|
+
# 3.9-only by nature, not by convenience: `Generic[K, V]` on 3.9 rejects a
|
|
330
|
+
# plain object ("Parameters to generic types must be types"), so the shim must
|
|
331
|
+
# subclass `TypeVar` -- and `TypeVar` stopped being subclassable in 3.12. The
|
|
332
|
+
# branch is unreachable on 3.10+ anyway, since `typing.ParamSpec` exists there.
|
|
333
|
+
#
|
|
334
|
+
# Runs in a subprocess with `typing_extensions` blocked so the fallback is
|
|
335
|
+
# exercised for real rather than mocked.
|
|
336
|
+
probe = textwrap.dedent(
|
|
337
|
+
"""
|
|
338
|
+
import builtins
|
|
339
|
+
|
|
340
|
+
_real_import = builtins.__import__
|
|
341
|
+
|
|
342
|
+
def _blocked(name, *a, **k):
|
|
343
|
+
if name == "typing_extensions":
|
|
344
|
+
raise ImportError("emulating an env without typing_extensions")
|
|
345
|
+
return _real_import(name, *a, **k)
|
|
346
|
+
|
|
347
|
+
builtins.__import__ = _blocked
|
|
348
|
+
|
|
349
|
+
from pathlib_next.utils import LRU, K
|
|
350
|
+
|
|
351
|
+
assert K.args is not None, "ParamSpec fallback lost .args"
|
|
352
|
+
assert K.kwargs is not None, "ParamSpec fallback lost .kwargs"
|
|
353
|
+
lru = LRU(lambda x: x * 2, maxsize=4)
|
|
354
|
+
assert lru(21) == 42
|
|
355
|
+
assert lru(21) == 42
|
|
356
|
+
lru.invalidate(21)
|
|
357
|
+
assert lru(21) == 42
|
|
358
|
+
print("FALLBACK_OK")
|
|
359
|
+
"""
|
|
360
|
+
)
|
|
361
|
+
result = subprocess.run(
|
|
362
|
+
[sys.executable, "-c", probe],
|
|
363
|
+
capture_output=True,
|
|
364
|
+
text=True,
|
|
365
|
+
cwd=pathlib.Path(utils.__file__).parents[3],
|
|
366
|
+
)
|
|
367
|
+
assert "FALLBACK_OK" in result.stdout, (
|
|
368
|
+
f"fallback import failed:\nstdout={result.stdout}\nstderr={result.stderr}"
|
|
369
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|