pathlib-next 0.9.4__tar.gz → 0.9.6__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.9.4 → pathlib_next-0.9.6}/CHANGELOG.md +70 -1
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/PKG-INFO +1 -1
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/divergences.md +6 -1
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/guides/schemes.md +11 -3
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/pyproject.toml +1 -1
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/AGENTS.md +47 -13
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/fspath.py +19 -4
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/path.py +65 -5
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/testing.py +7 -2
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/archive/_base.py +135 -46
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/utils/glob.py +32 -2
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_archive_safety.py +204 -8
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_glob_parity.py +84 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_pure_parity.py +35 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/.gitignore +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/AGENTS.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/LICENSE +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/README.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/cli.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/mempath.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/path.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/protocols.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/schemes/archive.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/schemes/ftp.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/schemes/git.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/schemes/http.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/schemes/local.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/schemes/objstore.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/schemes/sftp.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/testing.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/uri.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/api/utils.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/benchmarks.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/changelog.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/guides/cli.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/guides/extending.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/docs/index.md +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/examples/az_listing.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/examples/data_and_archive.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/examples/ftp_listing.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/examples/github_listing.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/examples/gitlab_listing.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/examples/gs_listing.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/examples/http_listing.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/examples/local_and_mem.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/examples/s3_listing.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/examples/sftp_sync.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/examples/webdav_roundtrip.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/mkdocs.yml +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/__init__.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/mempath.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/protocols/__init__.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/protocols/checksum.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/protocols/fs.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/protocols/io.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/py.typed +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/tools/__init__.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/tools/uripath.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/__init__.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/query.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/__init__.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/_gitrepo.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/archive/__init__.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/archive/tar.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/archive/zip.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/az.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/data.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/dav.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/file.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/ftp.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/git/__init__.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/git/_base.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/git/github.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/git/gitlab.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/github.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/gitlab.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/gs.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/http.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/s3.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/sftp/__init__.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/sftp/_asyncssh.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/sftp/_paramiko.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/schemes/sftp/_sshconfig.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/uri/source.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/utils/__init__.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/utils/archive.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/utils/checksum.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/utils/stat.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/src/pathlib_next/utils/sync.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/conftest.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_archive_parity.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_archive_uri.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_az.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_az_fake.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_checksum.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_contract.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_contract_helpers.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_data_uri.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_dav.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_destructive_safety.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_ftp.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_ftp_objstore_parity.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_gitrepo.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_glob.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_gs.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_gs_fake.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_http.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_http_live.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_http_parser.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_httpdav_safety.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_io_parity.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_local.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_low_core.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_low_schemes.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_low_sync.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_mempath.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_mro_precedence.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_parity_io.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_parity_pure.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_path_gaps.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_pathname.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_plugins.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_properties.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_query.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_routing.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_s3.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_sftp.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_sftp_asyncssh.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_sftp_transport.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_smoke.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_source.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_sync.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_sync_safety.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_sync_sftp.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_sync_sftp_parity.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_transport_security.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_uri_core_parity.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_uri_parse.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_uri_path.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_uripath_tool.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_utils.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_walk.py +0 -0
- {pathlib_next-0.9.4 → pathlib_next-0.9.6}/tests/test_webdav.py +0 -0
|
@@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.6] - 2026-09-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`Path.glob(None)` / `rglob(None)`**: expand the pattern the path itself
|
|
14
|
+
carries (`LocalPath("/etc/*.conf").glob(None)`), splitting at the first
|
|
15
|
+
wildcard. `glob("")` still raises `ValueError` as pathlib does -- `None`
|
|
16
|
+
is the spelling that cannot collide with a real pattern -- so this restores
|
|
17
|
+
what 0.9.4 removed as an explicit, supported form rather than by accident.
|
|
18
|
+
- **`Path.glob(native=)` / `rglob(native=)`**, default `True`: follow the
|
|
19
|
+
running interpreter on the two rules pathlib changed mid-series. A
|
|
20
|
+
trailing `/` is ignored before 3.11 and selects directories only from
|
|
21
|
+
3.11; a component that merely contains `**` (`a**`) raises `ValueError`
|
|
22
|
+
before 3.13 and is a plain wildcard from 3.13. `native=False` applies one
|
|
23
|
+
rule on every version instead, which is what a caller comparing results
|
|
24
|
+
across backends or interpreters wants.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- **`glob()` now matches the running interpreter exactly**, including on
|
|
28
|
+
Python 3.9-3.12 where it previously applied its own rule for a trailing
|
|
29
|
+
`/` and for `a**`. Measured with a 46-comparison differential sweep
|
|
30
|
+
against `pathlib`: 3.14 was already identical, and 3.9 went from 4
|
|
31
|
+
disagreements to 0. Pass `native=False` for the previous, version-
|
|
32
|
+
independent behaviour; `pathlib_next.testing`'s contract suite does.
|
|
33
|
+
|
|
34
|
+
### Documentation
|
|
35
|
+
- **Named the replacement for `glob("")`**, which 0.9.4 removed for pathlib
|
|
36
|
+
parity: `pathlib_next.utils.glob.glob(path, recursive=...)` expands a
|
|
37
|
+
pattern the path itself carries, splitting at the first wildcard. The
|
|
38
|
+
0.9.4 entry withdrew the capability without naming it, and `Path.glob()`'s
|
|
39
|
+
docstring documented the `ValueError` but not the alternative. Reported by
|
|
40
|
+
yaconfiglib, whose `path.glob("", recursive=...)` calls stopped working.
|
|
41
|
+
|
|
42
|
+
## [0.9.5] - 2026-09-16
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- **Archive member names are normalized**, so the same member is reachable
|
|
46
|
+
however the archive was written and whichever format it is. A leading
|
|
47
|
+
`./` (what `tar -C dir .`, `TarFile.add(arcname=".")` and
|
|
48
|
+
`shutil.make_archive` put on every member), empty segments (`a//b`) and
|
|
49
|
+
interior `.`/`..` (`a/./b`, `a/b/../c`) now resolve to one name for
|
|
50
|
+
listing and lookup alike. Previously only `tar:` stripped a leading `./`:
|
|
51
|
+
a zip written that way listed as empty and none of its members could be
|
|
52
|
+
read under any spelling, while a name such as `d//e.txt` was readable but
|
|
53
|
+
absent from listings.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
- **A drive- or backslash-shaped member name is no longer dropped.**
|
|
57
|
+
`C:drive.txt` and `a\b` are ordinary filenames on POSIX, and an archive
|
|
58
|
+
written there may contain them; they were silently absent from every
|
|
59
|
+
listing and unreadable on every platform. The rule they were failing is a
|
|
60
|
+
*destination* rule, and now lives where the joining happens:
|
|
61
|
+
`Path.copy(recursive=True)` refuses a child name that would not stay
|
|
62
|
+
inside its target (`ValueError` through `ignore_error`), which is what
|
|
63
|
+
`PathSyncer` and `utils.unpack_archive()` already did per destination.
|
|
64
|
+
Copying such a member onto a Windows path is still refused; copying it to
|
|
65
|
+
a POSIX path, a `MemPath` or another archive now works.
|
|
66
|
+
- **A member name that escapes the archive root** (`../x`, `/abs`, a
|
|
67
|
+
`..` with nothing left to consume) has no name inside the archive: it was
|
|
68
|
+
already never listed, and is now never readable either. Such a member
|
|
69
|
+
used to be hidden from listings while `read_bytes()` still returned it
|
|
70
|
+
under its raw name. (Writing to one already failed and created nothing;
|
|
71
|
+
that is unchanged.) A `..` that stays inside is resolved rather than
|
|
72
|
+
rejected (`pkg/../ok.txt` reads `ok.txt`), and when two spellings
|
|
73
|
+
normalize to one name the later member wins, as in `zipfile`/`tarfile`.
|
|
74
|
+
- **`Uri`'s RFC 3986 dot-segment removal is documented** as a divergence
|
|
75
|
+
from `pathlib` (`docs/divergences.md`); the behaviour is unchanged.
|
|
76
|
+
|
|
10
77
|
## [0.9.4] - 2026-09-16
|
|
11
78
|
|
|
12
79
|
### Fixed
|
|
@@ -1269,7 +1336,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
1269
1336
|
- Sync error handling.
|
|
1270
1337
|
- Generic Path Protocol based pathlib implementation for URI paths with file access support for sftp, http, file schemes.
|
|
1271
1338
|
|
|
1272
|
-
[Unreleased]: https://github.com/jose-pr/pathlib-next/compare/v0.9.
|
|
1339
|
+
[Unreleased]: https://github.com/jose-pr/pathlib-next/compare/v0.9.6...HEAD
|
|
1340
|
+
[0.9.6]: https://github.com/jose-pr/pathlib-next/compare/v0.9.5...v0.9.6
|
|
1341
|
+
[0.9.5]: https://github.com/jose-pr/pathlib-next/compare/v0.9.4...v0.9.5
|
|
1273
1342
|
[0.9.4]: https://github.com/jose-pr/pathlib-next/compare/v0.9.3...v0.9.4
|
|
1274
1343
|
[0.9.3]: https://github.com/jose-pr/pathlib-next/compare/v0.9.2...v0.9.3
|
|
1275
1344
|
[0.9.2]: https://github.com/jose-pr/pathlib-next/compare/v0.9.1...v0.9.2
|
|
@@ -55,7 +55,8 @@ these operations itself always keeps its own implementation.
|
|
|
55
55
|
| Method | pathlib behavior | Our behavior | Why |
|
|
56
56
|
| --- | --- | --- | --- |
|
|
57
57
|
| `Uri("http://h/d/").name` (trailing `/`) | `PurePosixPath("d/").name == "d"` | A trailing `/` is kept: `name` is `""` and `parent` is `http://h/d`. | For HTTP/WebDAV a trailing slash is how a directory URL is spelled; normalizing it away changes which URL is requested. |
|
|
58
|
-
| `Path.glob()` / `rglob()` edge cases | Version-dependent pathlib rules | Hidden entries are included by default (pathlib parity; `include_hidden=False` filters them).
|
|
58
|
+
| `Path.glob()` / `rglob()` edge cases | Version-dependent pathlib rules | Hidden entries are included by default (pathlib parity; `include_hidden=False` filters them). `recurse_symlinks=True` raises `NotImplementedError`: `**` never descends into directory symlinks. A trailing `**` follows the running interpreter (files too on 3.13+). The two rules pathlib changed mid-series follow the running interpreter by default (`native=True`): a trailing `/` is ignored before 3.11 and selects directories only from 3.11, and `a**` raises `ValueError` before 3.13 and is a plain wildcard from 3.13. `native=False` applies one rule on every version instead -- trailing `/` always selects directories only, `a**` is always a plain wildcard. | Loop-safe recursion without `st_dev`/`st_ino` (most backends' stats lack them) rules out following links. The `native` default keeps `LocalPath` answering exactly what the `pathlib` beside it answers; `native=False` is for a caller that wants one answer across backends and interpreters, which is what the contract suite asserts. |
|
|
59
|
+
| `Path.glob(None)` / `rglob(None)` | `pathlib` has no such form (its pattern is always applied to a directory) | `None` expands the pattern THIS PATH CARRIES: `LocalPath("/etc/*.conf").glob(None)` splits at the first wildcard and globs from there (`utils.glob.glob()`). `glob("")` still raises `ValueError`, as pathlib does. | An extension: a path that is itself a pattern is a common shape for config and CLI inputs, and before 0.9.4 `glob("")` was the accidental spelling for it. `None` cannot collide with a real pattern, so parity is untouched. |
|
|
59
60
|
| `Uri.query` | N/A (pathlib has no query) | The query is kept exactly as received (percent-encoded) and sent unchanged; `Query(...).decode()`/`to_dict()` decode. `Uri.parts` is `(source, path, query, fragment)`, not path segments (`segments` is). A `%2F` in a path decodes to `/` and is not distinguishable from a separator. | Decoding at parse time and re-encoding changed what reached the server (a signed URL's `%2B` became `+`, an escaped `&` split a value). The decoded-path model cannot keep `%2F` distinct. |
|
|
60
61
|
| `Path.copy(follow_symlinks=False)` on a symlink | CPython 3.14: copies the link as a link | Same: the link is recreated (not its metadata). Where the source cannot `readlink()` or the target cannot create links, raises `NotImplementedError` instead of copying content. `copy(recursive=True)` into its own subtree raises `OSError(EINVAL)` before creating anything. | Copying the link target's content under the link's permissions produced a 0o777 regular file. |
|
|
61
62
|
| `SftpPath.rename(target)` onto an existing file | POSIX `rename(2)`: replaces | Replaces through `posix-rename@openssh.com` where the server supports it; otherwise raises `FileExistsError`. | Plain SFTPv3 rename refuses an existing target with an uninformative failure. |
|
|
@@ -80,6 +81,10 @@ these operations itself always keeps its own implementation.
|
|
|
80
81
|
| `str()`/`repr()` of `github:`/`gitlab:`/`git:` paths | N/A (no pathlib equivalent) | The **whole** userinfo is redacted, not just the part after `:` as for other schemes. `as_uri()` (unsanitized) still returns it. | The token is commonly the bare userinfo (`TOKEN@host`), the one part other schemes keep, so it reached logs and tracebacks. |
|
|
81
82
|
| `Path.exists()` / `is_*()` on a stat error other than "not found" | pathlib 3.9-3.12 re-raise errors outside ENOENT/ENOTDIR/EBADF/ELOOP (e.g. `PermissionError`); 3.13+ return `False` | Every `OSError`/`ValueError` from `stat()` returns `False` on every Python version, including `LocalPath.exists()` on 3.9-3.12. | One rule across backends and versions, matching current pathlib. |
|
|
82
83
|
| `PathSyncer` directory → file/symlink type change | N/A (closest: `rsync`, which will not delete a non-empty directory without `--delete`/`--force`) | With `remove_missing=False`, a non-empty target directory is not replaced when the source entry at that name became a file or symlink: `IsADirectoryError` goes through `ignore_error` (`SyncEvent.TypeMismatch`) and the directory is kept. Empty directories, or `remove_missing=True`, are replaced. | `remove_missing=False` means "never delete target-only data"; replacing the directory silently deleted its whole subtree. |
|
|
84
|
+
| `Path.copy(recursive=True)` child names | `shutil.copytree` joins whatever the listing yields | A child name that would not stay inside `target` is refused with `ValueError` through `ignore_error`: `..`, and `\`/`:`/a trailing dot when the target reads names with Windows rules. | The names come from listings the destination does not control (an archive, an HTTP index, an object-store key); on a Windows target `"C:x"` joins to a drive-relative path outside the destination entirely. `PathSyncer` and `utils.unpack_archive()` already applied this per destination. |
|
|
85
|
+
| Archive member names | N/A (`zipfile`/`tarfile` expose the raw name as written) | Normalized as POSIX relative paths for every format: a leading `./`, empty segments and interior `.`/`..` resolve, so one member has one name and listings and lookups agree; the raw spelling still addresses it, and the later of two members that normalize alike wins. A name that escapes the root (`../x`, `/abs`) has no name inside the archive at all, while a drive- or backslash-shaped name is a normal member (an ordinary POSIX filename) that only a Windows destination refuses to receive. | The same file was reachable or not depending on how the writer spelled it: a zip written by `shutil.make_archive`-style `./` prefixes listed as empty, and zip and tar disagreed about identical archives. |
|
|
86
|
+
| The POSIX `//` root | `PurePosixPath("//")` keeps `//` as a root distinct from `/` (POSIX leaves exactly two leading slashes implementation-defined; three or more collapse) | The generic classes do not model it: `MemPath("//")` collapses to `/`, and `Uri("//")` reads `//` as the start of an authority (RFC 3986), giving an empty authority and an empty path -- so `Uri("//a/b")` has host `a` and path `/b`. `match()` therefore disagrees with `pathlib` on that one path. `LocalPath`/`WindowsPathname` are unaffected (they inherit pathlib's parsing, where `//server/share` is a UNC drive). | A distinct double-slash root has no meaning for an in-memory tree or a URI, and for a `Uri` it cannot: `//a/b` must read `a` as a host. Modelling it would change segment normalization everywhere (`parents`, `relative_to`, `is_absolute`, every scheme) to serve a spelling no backend can use. |
|
|
87
|
+
| `Uri` path dot segments | `pathlib` keeps `..` lexically (`PurePosixPath("a/../b")` is `a/../b`) | `Uri` removes dot segments as RFC 3986 requires of a URI reference, in the constructor and in `/`-joins: `Uri("a/../b")` is `b`, `Uri("http://h/x") / "a/../b"` is `http://h/x/b`, and `Uri("a/b/..")` is `a/`. A leading `..` that would pass the root is kept, not resolved. `MemPath` follows `pathlib` instead. | A URI is resolved, not spelled: `..` in a URI reference has a defined meaning that servers, caches and proxies already apply, so keeping it lexically would address a different resource than the same string typed into a browser. |
|
|
83
88
|
| Nested archive URIs | N/A | Each leading archive scheme in `<archive-uri>` consumes one `!/` (`zip:zip:file:///outer.zip!/inner.zip!/x.txt`); a member name containing `!/` is written `%21/`. Nested archives are read-only. | The first `!/` was always taken as the separator, so an archive inside an archive could not be addressed. |
|
|
84
89
|
| Object-store key that is both an object and a prefix | N/A (a filesystem entry has one type) | `iterdir()`/`walk()`/`copy(recursive=True)` on `s3:`/`gs:`/`az:` show only the object (`x`), matching `stat()`'s exact-object precedence; the subtree under `x/` is not listed. | Listings used to keep the directory and drop the object, contradicting `stat()`. |
|
|
85
90
|
| `HttpPath.iterdir()` on a file | pathlib raises `NotADirectoryError` | Raises `NotADirectoryError` for a non-HTML response, without downloading it. An HTML file cannot be told apart from an index page and lists as empty. | HTTP has no directory type; the response content type is the only signal. |
|
|
@@ -205,9 +205,17 @@ The `<archive-uri>` is any absolute URI with an explicit scheme, so
|
|
|
205
205
|
- An archive inside an archive is addressed by nesting
|
|
206
206
|
(`zip:zip:file:///outer.zip!/inner.zip!/x.txt`) and is read-only; a `!/`
|
|
207
207
|
inside a member name is written `%21/`.
|
|
208
|
-
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
- Member names are normalized as POSIX relative paths, the same way for
|
|
209
|
+
every format: `./x`, `a//b`, `a/./b` and `a/b/../c` all resolve, so a
|
|
210
|
+
member lists and reads under one name however the archive was written
|
|
211
|
+
(`tar -C dir .` and `shutil.make_archive` prefix every member with `./`).
|
|
212
|
+
The spelling as written still works.
|
|
213
|
+
- Members whose names would escape the archive (`..` past the root, or an
|
|
214
|
+
absolute path) have no name inside it: never listed, never readable. A
|
|
215
|
+
name that merely a Windows destination would misread -- `C:drive.txt`, or
|
|
216
|
+
one containing `\` -- is a normal member, since both are ordinary
|
|
217
|
+
filenames on POSIX; copying or extracting it onto a Windows path is what
|
|
218
|
+
refuses it. Exception types are the POSIX ones on every platform.
|
|
211
219
|
|
|
212
220
|
## Git hosting
|
|
213
221
|
|
|
@@ -13,7 +13,7 @@ build-backend = "hatchling.build"
|
|
|
13
13
|
# `import pathlib_next`) -- a hyphen is not legal in a Python identifier.
|
|
14
14
|
# Distribution name and import name differing is ordinary and intended.
|
|
15
15
|
name = "pathlib-next"
|
|
16
|
-
version = "0.9.
|
|
16
|
+
version = "0.9.6"
|
|
17
17
|
authors = [{ name = "Jose A" }]
|
|
18
18
|
description = "Generic Path Protocol based pathlib"
|
|
19
19
|
readme = "README.md"
|
|
@@ -73,16 +73,28 @@ silently absent and `from pathlib_next.uri import UriPath` raises
|
|
|
73
73
|
`PathSyncer`; override it when the listing call already returns metadata.
|
|
74
74
|
`None` means "unknown", never "missing".
|
|
75
75
|
- `glob(pattern, *, case_sensitive=None, include_hidden=True,
|
|
76
|
-
recursive=None, dironly=None, recurse_symlinks=False)` —
|
|
77
|
-
semantics: hidden entries included,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
recursive=None, dironly=None, recurse_symlinks=False, native=True)` —
|
|
77
|
+
pathlib semantics: hidden entries included, `**` never descends into
|
|
78
|
+
directory symlinks (`recurse_symlinks=True` → `NotImplementedError`), a
|
|
79
|
+
trailing `**` selects files too on 3.13+, a missing or non-directory base
|
|
80
|
+
yields nothing, `""` → `ValueError`, an absolute pattern →
|
|
81
|
+
`glob.NonRelativePatternError`. `recursive=None` enables recursion when a
|
|
82
|
+
component is `**`; an explicit value wins. Validates eagerly, selects
|
|
83
|
+
lazily. On a remote scheme a recursive glob lists every directory of the
|
|
84
|
+
subtree.
|
|
85
|
+
- **`pattern=None`** expands the pattern THIS PATH CARRIES
|
|
86
|
+
(`LocalPath("/etc/*.conf").glob(None)`), splitting at the first
|
|
87
|
+
wildcard — the supported form for a path that is itself a pattern.
|
|
88
|
+
`""` still raises.
|
|
89
|
+
- **`native=True`** (default) follows the running interpreter on the two
|
|
90
|
+
rules pathlib changed mid-series: a trailing `/` is ignored before 3.11
|
|
91
|
+
and selects directories only from 3.11; `a**` raises `ValueError`
|
|
92
|
+
before 3.13 and is a plain wildcard from 3.13. `native=False` applies
|
|
93
|
+
one rule on every version (trailing `/` → directories only, `a**` → a
|
|
94
|
+
plain wildcard), so a pattern answers the same on every interpreter and
|
|
95
|
+
backend; `pathlib_next.testing`'s contract suite uses it.
|
|
85
96
|
- `rglob(pattern, **same_kwargs)` — `glob(f"**/{pattern}", recursive=True)`.
|
|
97
|
+
`pattern=None` is `glob(None)`.
|
|
86
98
|
- `walk(top_down=True, on_error=None, follow_symlinks=False)` — drives
|
|
87
99
|
`_scandir()`; its stats are trusted only with `follow_symlinks=False`. No
|
|
88
100
|
symlink-cycle protection when following (only `LocalPath` has pathlib's).
|
|
@@ -114,6 +126,13 @@ silently absent and `from pathlib_next.uri import UriPath` raises
|
|
|
114
126
|
the same file (or a case-insensitive alias) → `OSError(EINVAL)`.
|
|
115
127
|
- The source is opened before the target is touched; a failed stream
|
|
116
128
|
removes the partial target.
|
|
129
|
+
- A recursive copy refuses any child name that would not stay inside
|
|
130
|
+
`target` — `..`, and `\`/`:`/a trailing dot when the target reads
|
|
131
|
+
names with Windows rules (`utils.is_windows_flavoured()`). The names
|
|
132
|
+
come from a listing the destination does not control (an archive, a
|
|
133
|
+
remote index, an object-store key), and on a Windows target `"C:x"`
|
|
134
|
+
joins to a drive-relative path outside it. Raised as `ValueError`
|
|
135
|
+
through `ignore_error`, per child, like any other child failure.
|
|
117
136
|
- `follow_symlinks=False` on a symlink recreates the link
|
|
118
137
|
(`NotImplementedError` if either side cannot).
|
|
119
138
|
- `preserve_metadata=True` copies permission bits only, and only a mode the
|
|
@@ -481,15 +500,30 @@ chained (their text can carry credentials).
|
|
|
481
500
|
- One shared handle per archive (keyed by the real local path, or the outer
|
|
482
501
|
URI), released when no path references it. A non-local outer is read into
|
|
483
502
|
memory.
|
|
484
|
-
-
|
|
485
|
-
|
|
503
|
+
- **Member names are normalized POSIX relative paths**, whatever the
|
|
504
|
+
writer emitted and whichever format: a leading `./` (`tar -C dir .`,
|
|
505
|
+
`shutil.make_archive`), empty segments (`a//b`) and interior `.`/`..`
|
|
506
|
+
(`a/./b`, `a/b/../c`) resolve, so one member has one name and a listing
|
|
507
|
+
and a lookup always agree. The spelling as written still addresses the
|
|
508
|
+
member. A name that would leave the root -- `../x`, `/abs`, or a `..`
|
|
509
|
+
with nothing to spend it on -- has no name inside the archive: it is
|
|
510
|
+
never listed, never readable, and cannot be written (the write fails and
|
|
511
|
+
creates nothing). A name only a *Windows destination* would misread
|
|
512
|
+
(`C:drive.txt`, `a\b`) IS a member, because it is an ordinary POSIX
|
|
513
|
+
filename; refusing to join it is the destination's rule, applied by
|
|
514
|
+
whatever writes there (see `copy()` below, `PathSyncer`,
|
|
515
|
+
`unpack_archive()`). `zipfile` itself rewrites `\` to `/`, so a
|
|
516
|
+
backslash name only survives in a tar. When two spellings normalize to one name the
|
|
517
|
+
later member wins, as in `zipfile`/`tarfile`. Exception types are POSIX on every
|
|
518
|
+
platform.
|
|
486
519
|
- Writes: zip only, and only with a local `file:` outer (else
|
|
487
520
|
`NotImplementedError`). `"w"`/`"x"`/`"r+"`, `mkdir()`, `unlink()`,
|
|
488
521
|
`rmdir()`, `rename()` (same archive; replaces like POSIX `rename`);
|
|
489
522
|
parents must exist; `"a"` unsupported. Every mutation replaces the archive
|
|
490
523
|
atomically (temp file + `os.replace`) and keeps other members' metadata,
|
|
491
|
-
the comment and any prefix bytes.
|
|
492
|
-
|
|
524
|
+
the comment and any prefix bytes. A write uses the normalized name; a
|
|
525
|
+
name that escapes the root fails and creates nothing.
|
|
526
|
+
`tar:` (plain, gz, bz2, xz) is read-only.
|
|
493
527
|
|
|
494
528
|
## CLI (`uripath`, `pathlib_next.tools.uripath`)
|
|
495
529
|
|
|
@@ -394,21 +394,35 @@ class LocalPath(
|
|
|
394
394
|
|
|
395
395
|
def glob(
|
|
396
396
|
self,
|
|
397
|
-
pattern: str | _proto.FsPathLike,
|
|
397
|
+
pattern: "str | _proto.FsPathLike | None",
|
|
398
398
|
*,
|
|
399
399
|
case_sensitive: bool = None,
|
|
400
400
|
include_hidden: bool = True,
|
|
401
401
|
recursive: bool = None,
|
|
402
402
|
dironly: bool = None,
|
|
403
403
|
recurse_symlinks: bool = False,
|
|
404
|
+
native: bool = True,
|
|
404
405
|
):
|
|
405
406
|
"""Iterate over this subtree and yield all existing files (of any
|
|
406
407
|
kind, including directories) matching the given relative pattern.
|
|
407
408
|
|
|
408
|
-
Same semantics as Path.glob()
|
|
409
|
-
|
|
410
|
-
|
|
409
|
+
Same semantics as Path.glob(), including `pattern=None` (expand the
|
|
410
|
+
pattern this path carries) and `native=` (follow the running
|
|
411
|
+
interpreter, or one rule on every version); every separator of this
|
|
412
|
+
flavour splits the pattern, and a pattern with a drive or root
|
|
413
|
+
raises `glob.NonRelativePatternError` like pathlib.
|
|
411
414
|
"""
|
|
415
|
+
if pattern is None:
|
|
416
|
+
return _proto.Path.glob(
|
|
417
|
+
self,
|
|
418
|
+
None,
|
|
419
|
+
case_sensitive=case_sensitive,
|
|
420
|
+
include_hidden=include_hidden,
|
|
421
|
+
recursive=recursive,
|
|
422
|
+
dironly=dironly,
|
|
423
|
+
recurse_symlinks=recurse_symlinks,
|
|
424
|
+
native=native,
|
|
425
|
+
)
|
|
412
426
|
pattern = _os.fspath(pattern)
|
|
413
427
|
if pattern:
|
|
414
428
|
anchored = self.with_segments(pattern)
|
|
@@ -430,4 +444,5 @@ class LocalPath(
|
|
|
430
444
|
recursive=recursive,
|
|
431
445
|
dironly=dironly,
|
|
432
446
|
recurse_symlinks=recurse_symlinks,
|
|
447
|
+
native=native,
|
|
433
448
|
)
|
|
@@ -704,13 +704,14 @@ class Path(Pathname, Chmod, Stat, BinaryOpen):
|
|
|
704
704
|
|
|
705
705
|
def glob(
|
|
706
706
|
self,
|
|
707
|
-
pattern: str | _ty.Self,
|
|
707
|
+
pattern: str | _ty.Self | None,
|
|
708
708
|
*,
|
|
709
709
|
case_sensitive: bool = None,
|
|
710
710
|
include_hidden: bool = True,
|
|
711
711
|
recursive: bool = None,
|
|
712
712
|
dironly: bool = None,
|
|
713
713
|
recurse_symlinks: bool = False,
|
|
714
|
+
native: bool = True,
|
|
714
715
|
):
|
|
715
716
|
"""Iterate over this subtree and yield all existing files (of any
|
|
716
717
|
kind, including directories) matching the given relative pattern.
|
|
@@ -724,6 +725,20 @@ class Path(Pathname, Chmod, Stat, BinaryOpen):
|
|
|
724
725
|
`glob.NonRelativePatternError` (a `NotImplementedError` and a
|
|
725
726
|
`ValueError`). `recurse_symlinks=True` is not supported.
|
|
726
727
|
|
|
728
|
+
`pattern=None` expands the pattern THIS PATH CARRIES
|
|
729
|
+
(`LocalPath("/etc/*.conf").glob(None)`) instead of applying one to a
|
|
730
|
+
directory: the path is split at its first wildcard and globbed from
|
|
731
|
+
there (`utils.glob.glob()`). `""` still raises, as pathlib does.
|
|
732
|
+
|
|
733
|
+
`native=True` (the default) follows the running interpreter on the
|
|
734
|
+
two rules pathlib changed mid-series: a trailing "/" is ignored
|
|
735
|
+
before 3.11, and a component that merely contains "**" ("a**")
|
|
736
|
+
raises `ValueError` before 3.13. `native=False` applies one rule on
|
|
737
|
+
every version -- trailing "/" always selects directories only, "a**"
|
|
738
|
+
is always a plain wildcard -- so a pattern answers the same
|
|
739
|
+
everywhere, which is what a cross-backend or cross-version caller
|
|
740
|
+
usually wants.
|
|
741
|
+
|
|
727
742
|
A "**" component auto-enables recursion. Pass `recursive=False`
|
|
728
743
|
explicitly to treat "**" as a plain "*" instead.
|
|
729
744
|
Note for remote schemes (http/sftp): a recursive glob walks the
|
|
@@ -731,10 +746,21 @@ class Path(Pathname, Chmod, Stat, BinaryOpen):
|
|
|
731
746
|
"""
|
|
732
747
|
if recurse_symlinks:
|
|
733
748
|
raise NotImplementedError("glob(recurse_symlinks=True)")
|
|
749
|
+
if pattern is None:
|
|
750
|
+
# The pattern is the path itself; `glob()` splits at the first
|
|
751
|
+
# wildcard, so an absolute one is fine here (unlike a pattern
|
|
752
|
+
# argument, which must stay relative to self).
|
|
753
|
+
return _glob.glob(
|
|
754
|
+
self,
|
|
755
|
+
recursive=bool(recursive),
|
|
756
|
+
include_hidden=include_hidden,
|
|
757
|
+
case_sensitive=case_sensitive,
|
|
758
|
+
dironly=bool(dironly),
|
|
759
|
+
)
|
|
734
760
|
# Validates eagerly (like pathlib 3.13+); the returned selection is
|
|
735
761
|
# lazy. The pattern is never joined onto self: `self / pattern` let an
|
|
736
762
|
# absolute pattern escape self and re-parsed "?" as a URI query.
|
|
737
|
-
parts, trailing_sep = _glob.parse_pattern(pattern)
|
|
763
|
+
parts, trailing_sep = _glob.parse_pattern(pattern, native=native)
|
|
738
764
|
if recursive is None:
|
|
739
765
|
recursive = _glob.RECURSIVE in parts
|
|
740
766
|
return _glob.select(
|
|
@@ -748,19 +774,33 @@ class Path(Pathname, Chmod, Stat, BinaryOpen):
|
|
|
748
774
|
|
|
749
775
|
def rglob(
|
|
750
776
|
self,
|
|
751
|
-
pattern: str,
|
|
777
|
+
pattern: str | None,
|
|
752
778
|
*,
|
|
753
779
|
case_sensitive: bool = None,
|
|
754
780
|
include_hidden: bool = True,
|
|
755
781
|
recursive: bool = True,
|
|
756
782
|
dironly: bool = None,
|
|
757
783
|
recurse_symlinks: bool = False,
|
|
784
|
+
native: bool = True,
|
|
758
785
|
):
|
|
759
|
-
"""Equivalent to `glob(f"**/{pattern}", recursive=True)`.
|
|
786
|
+
"""Equivalent to `glob(f"**/{pattern}", recursive=True)`.
|
|
787
|
+
|
|
788
|
+
`pattern=None` and `native=` mean what they do on `glob()`; with
|
|
789
|
+
`None` this is `glob(None, recursive=True)`, since a carried pattern
|
|
790
|
+
brings its own anchor and nothing can be prefixed to it."""
|
|
791
|
+
if pattern is None:
|
|
792
|
+
return self.glob(
|
|
793
|
+
None,
|
|
794
|
+
case_sensitive=case_sensitive,
|
|
795
|
+
include_hidden=include_hidden,
|
|
796
|
+
recursive=recursive,
|
|
797
|
+
dironly=dironly,
|
|
798
|
+
recurse_symlinks=recurse_symlinks,
|
|
799
|
+
)
|
|
760
800
|
if not (isinstance(pattern, str) and not pattern):
|
|
761
801
|
# Reject an absolute pattern before "**/" hides its anchor;
|
|
762
802
|
# glob() validates without listing anything.
|
|
763
|
-
self.glob(pattern, recursive=False)
|
|
803
|
+
self.glob(pattern, recursive=False, native=native)
|
|
764
804
|
return self.glob(
|
|
765
805
|
f"**/{pattern}",
|
|
766
806
|
case_sensitive=case_sensitive,
|
|
@@ -768,6 +808,7 @@ class Path(Pathname, Chmod, Stat, BinaryOpen):
|
|
|
768
808
|
recursive=recursive,
|
|
769
809
|
dironly=dironly,
|
|
770
810
|
recurse_symlinks=recurse_symlinks,
|
|
811
|
+
native=native,
|
|
771
812
|
)
|
|
772
813
|
|
|
773
814
|
def walk(
|
|
@@ -1180,8 +1221,27 @@ class Path(Pathname, Chmod, Stat, BinaryOpen):
|
|
|
1180
1221
|
raise FileExistsError(target)
|
|
1181
1222
|
else:
|
|
1182
1223
|
target.mkdir()
|
|
1224
|
+
windows_target = _utils.is_windows_flavoured(target)
|
|
1183
1225
|
for child in children:
|
|
1184
1226
|
try:
|
|
1227
|
+
# The names come from a listing the destination does not
|
|
1228
|
+
# control (an archive, a remote index, an object-store
|
|
1229
|
+
# key), so one that is not a single component inside
|
|
1230
|
+
# `target` must never be joined onto it: on a Windows
|
|
1231
|
+
# target "C:x" joins to a drive-relative path outside it
|
|
1232
|
+
# entirely, and so does "a\\b". Reported through
|
|
1233
|
+
# `ignore_error` like any other per-child failure, not
|
|
1234
|
+
# silently skipped. `PathSyncer` and
|
|
1235
|
+
# `utils.unpack_archive()` apply the same rule per
|
|
1236
|
+
# destination; an archive listing keeps such names,
|
|
1237
|
+
# because they are ordinary filenames on POSIX.
|
|
1238
|
+
if not _utils.is_safe_child_name(
|
|
1239
|
+
child.name, windows=windows_target
|
|
1240
|
+
):
|
|
1241
|
+
raise ValueError(
|
|
1242
|
+
f"refusing unsafe child name {child.name!r} "
|
|
1243
|
+
f"under {target}"
|
|
1244
|
+
)
|
|
1185
1245
|
child.copy(
|
|
1186
1246
|
target / child.name,
|
|
1187
1247
|
overwrite=overwrite,
|
|
@@ -239,8 +239,13 @@ class ReadPathContract(PurePathContract):
|
|
|
239
239
|
"empty_dir",
|
|
240
240
|
}
|
|
241
241
|
assert {_rel(root, p) for p in root.glob("sub/*.py")} == {"sub/c.py"}
|
|
242
|
-
# A trailing separator selects directories only
|
|
243
|
-
|
|
242
|
+
# A trailing separator selects directories only -- with `native=False`,
|
|
243
|
+
# the one rule for every interpreter. The default follows the running
|
|
244
|
+
# one, and pathlib ignored a trailing separator before 3.11.
|
|
245
|
+
assert {_rel(root, p) for p in root.glob("*/", native=False)} == {
|
|
246
|
+
"sub",
|
|
247
|
+
"empty_dir",
|
|
248
|
+
}
|
|
244
249
|
|
|
245
250
|
def test_glob_recursive(self, root):
|
|
246
251
|
self._require("supports_listing")
|