pathlib-next 0.8.4__tar.gz → 0.8.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.8.4 → pathlib_next-0.8.6}/.gitignore +2 -2
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/CHANGELOG.md +67 -1
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/PKG-INFO +1 -1
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/divergences.md +41 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/pyproject.toml +10 -1
- pathlib_next-0.8.6/src/pathlib_next/AGENTS.md +270 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/fspath.py +31 -3
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/path.py +105 -10
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/utils/__init__.py +32 -1
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/utils/sync.py +56 -14
- pathlib_next-0.8.6/tests/test_mro_precedence.py +250 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_parity_io.py +11 -2
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_path_gaps.py +98 -9
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_pathname.py +13 -6
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_sync.py +96 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/LICENSE +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/README.md +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/api/mempath.md +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/api/path.md +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/api/testing.md +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/api/uri.md +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/api/utils.md +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/benchmarks.md +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/changelog.md +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/guides/cli.md +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/guides/extending.md +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/guides/schemes.md +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/index.md +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/az_listing.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/data_and_archive.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/ftp_listing.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/github_listing.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/gitlab_listing.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/gs_listing.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/http_listing.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/local_and_mem.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/s3_listing.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/sftp_sync.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/webdav_roundtrip.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/mkdocs.yml +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/__init__.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/mempath.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/protocols/__init__.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/protocols/fs.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/protocols/io.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/py.typed +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/testing.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/tools/__init__.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/tools/uripath.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/__init__.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/query.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/__init__.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/_gitrepo.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/archive/__init__.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/archive/_base.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/archive/tar.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/archive/zip.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/az.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/data.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/dav.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/file.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/ftp.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/git/__init__.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/git/_base.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/git/github.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/git/gitlab.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/github.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/gitlab.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/gs.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/http.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/s3.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/sftp/__init__.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/sftp/_asyncssh.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/sftp/_paramiko.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/sftp/_sshconfig.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/source.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/utils/archive.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/utils/checksum.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/utils/glob.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/utils/stat.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/conftest.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_archive_uri.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_az.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_az_fake.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_contract.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_data_uri.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_dav.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_ftp.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_gitrepo.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_glob.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_gs.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_gs_fake.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_http.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_http_live.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_http_parser.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_local.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_mempath.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_parity_pure.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_plugins.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_properties.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_query.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_s3.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_sftp.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_sftp_asyncssh.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_smoke.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_source.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_uri_parse.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_uri_path.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_uripath_tool.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_utils.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_walk.py +0 -0
- {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_webdav.py +0 -0
|
@@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.6] - 2026-07-26
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`PathSyncer.sync()` raised `TypeError` instead of honoring
|
|
14
|
+
`ignore_error`.** `sync()`'s `ignore_error` parameter defaulted to the bool
|
|
15
|
+
`False` and the symlink branch *called* it directly, so
|
|
16
|
+
`PathSyncer(ignore_error=True).sync(src, dst)` on a symlink source raised
|
|
17
|
+
`TypeError: 'bool' object is not callable` rather than the intended
|
|
18
|
+
`NotImplementedError`. The parameter now defaults to `None`, meaning "use
|
|
19
|
+
the policy given to `__init__`" -- it no longer silently shadows a
|
|
20
|
+
constructor-supplied policy -- and every branch consults one resolved
|
|
21
|
+
callable. Passing a callable explicitly behaves exactly as before.
|
|
22
|
+
- **`Path.copy()` now accepts a bool for `ignore_error`**, matching
|
|
23
|
+
`Path.rm()`'s bool-or-callable contract. Previously only a callable or
|
|
24
|
+
`None` was handled, so `copy(recursive=True, ignore_error=True)` broke as
|
|
25
|
+
soon as a child copy failed. `None` keeps its documented meaning (fail on
|
|
26
|
+
the first error), and a callable remains a notification hook whose return
|
|
27
|
+
value is not consulted, so existing handlers such as `errors.append` are
|
|
28
|
+
unaffected.
|
|
29
|
+
- **Downstream `Path` subclasses resolved stdlib `pathlib` operations
|
|
30
|
+
instead of pathlib_next's.** Concrete path classes mix a `pathlib` class
|
|
31
|
+
with `pathlib_next.Path`, so the MRO decided which library implemented a
|
|
32
|
+
method -- and which one won changed with the interpreter version. On
|
|
33
|
+
Python 3.14 the new stdlib `copy()`/`move()` displaced ours, crashing with
|
|
34
|
+
`AttributeError: ... has no attribute '_copy_from'` on non-local backends
|
|
35
|
+
and *silently* applying stdlib's different timestamp semantics on local
|
|
36
|
+
ones (making mtime-based syncs converge on 3.14 but never on <=3.13). In
|
|
37
|
+
the opposite direction, older stdlib lacked keywords this library's
|
|
38
|
+
protocols promise: `exists(follow_symlinks=)` (3.12+),
|
|
39
|
+
`read_text`/`write_text`'s `newline=` (3.13+), and `rglob`'s
|
|
40
|
+
`include_hidden=`/`recursive=`/`dironly=` extensions (never in stdlib), all
|
|
41
|
+
raising `TypeError` on the 3.9 floor. `Path.__init_subclass__` now
|
|
42
|
+
re-asserts the pathlib_next implementation of `copy`, `move`, `exists`,
|
|
43
|
+
`rglob`, `read_text` and `write_text` for any subclass that would otherwise
|
|
44
|
+
inherit stdlib's, so downstream implementers get correct behavior without
|
|
45
|
+
hand-written forwarding methods. A subclass or mixin that defines one of
|
|
46
|
+
these operations itself is never displaced.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
- `utils.as_error_handler()` centralizes `ignore_error` bool-to-callable
|
|
50
|
+
normalization. Callable **arities remain deliberately different per call
|
|
51
|
+
site** (`rm` -> `(error, path)`, `copy` -> `(error)`, `PathSyncer` ->
|
|
52
|
+
`(error, source, target, event)`); only the bool case is normalized, so no
|
|
53
|
+
public signature changed.
|
|
54
|
+
|
|
55
|
+
## [0.8.5] - 2026-07-26
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
- **`LocalPath.copy()` and `LocalPath.move()` resolved to the incompatible
|
|
59
|
+
stdlib implementations on Python 3.14.** Python 3.14 added methods with
|
|
60
|
+
those names ahead of `pathlib_next.Path` in `LocalPath`'s MRO, so calls using
|
|
61
|
+
pathlib_next extensions such as `overwrite=` or `recursive=` failed with
|
|
62
|
+
`TypeError`. `LocalPath` now routes both methods explicitly through the
|
|
63
|
+
pathlib_next implementations on every supported Python version.
|
|
64
|
+
- Generic paths now follow Python 3.14's updated `PurePath.with_suffix(".")`
|
|
65
|
+
behavior while retaining the earlier `ValueError` behavior on older Python
|
|
66
|
+
versions.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
- Documented that stdlib inheritance is deliberately local-only:
|
|
70
|
+
`LocalPath` is a real `pathlib.Path`, while URI, in-memory, and other virtual
|
|
71
|
+
implementations inherit the generic pathlib_next contracts without claiming
|
|
72
|
+
local-filesystem semantics.
|
|
73
|
+
|
|
10
74
|
## [0.8.4] - 2026-07-18
|
|
11
75
|
|
|
12
76
|
### Changed
|
|
@@ -516,7 +580,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
516
580
|
- Sync error handling.
|
|
517
581
|
- Generic Path Protocol based pathlib implementation for URI paths with file access support for sftp, http, file schemes.
|
|
518
582
|
|
|
519
|
-
[Unreleased]: https://github.com/jose-pr/pathlib_next/compare/v0.8.
|
|
583
|
+
[Unreleased]: https://github.com/jose-pr/pathlib_next/compare/v0.8.6...HEAD
|
|
584
|
+
[0.8.6]: https://github.com/jose-pr/pathlib_next/compare/v0.8.5...v0.8.6
|
|
585
|
+
[0.8.5]: https://github.com/jose-pr/pathlib_next/compare/v0.8.4...v0.8.5
|
|
520
586
|
[0.8.4]: https://github.com/jose-pr/pathlib_next/compare/v0.8.3...v0.8.4
|
|
521
587
|
[0.8.3]: https://github.com/jose-pr/pathlib_next/compare/v0.8.2...v0.8.3
|
|
522
588
|
[0.8.2]: https://github.com/jose-pr/pathlib_next/compare/v0.8.1...v0.8.2
|
|
@@ -11,6 +11,47 @@ in via MRO, so unless noted otherwise it behaves exactly like `pathlib.Path`
|
|
|
11
11
|
(it inherits the real implementation for anything not explicitly overridden).
|
|
12
12
|
The divergences below apply to `Uri`/`UriPath` and `MemPath`.
|
|
13
13
|
|
|
14
|
+
## Type relationships
|
|
15
|
+
|
|
16
|
+
Stdlib inheritance is deliberately limited to local filesystem paths:
|
|
17
|
+
|
|
18
|
+
- `LocalPath` subclasses both `pathlib.Path` and `pathlib_next.Path`.
|
|
19
|
+
- `PosixPathname` and `WindowsPathname` subclass the matching stdlib
|
|
20
|
+
`PurePath` classes and `pathlib_next.Pathname`.
|
|
21
|
+
- `MemPath`, `Uri`, `UriPath`, and custom virtual or remote implementations
|
|
22
|
+
subclass the generic pathlib_next bases, not `pathlib.Path`/`PurePath`.
|
|
23
|
+
- A plain stdlib `pathlib.Path` is not a `pathlib_next.Path`.
|
|
24
|
+
|
|
25
|
+
The generic classes cannot safely inherit the stdlib classes: pathlib parses
|
|
26
|
+
OS-specific path syntax and supplies operations whose semantics assume a local
|
|
27
|
+
filesystem, neither of which applies to a URI, archive member, object-store key,
|
|
28
|
+
or in-memory path. Registering stdlib paths as virtual `pathlib_next.Path`
|
|
29
|
+
subclasses would likewise promise pathlib_next's extended operation contract on
|
|
30
|
+
Python versions where stdlib paths do not implement it. Code accepting every
|
|
31
|
+
implementation should type against `pathlib_next.Path` or its documented
|
|
32
|
+
protocols; code requiring an OS path should type against `pathlib.Path`.
|
|
33
|
+
|
|
34
|
+
### Operation precedence in `Path` subclasses
|
|
35
|
+
|
|
36
|
+
Because concrete classes mix a `pathlib` class with `pathlib_next.Path`, the
|
|
37
|
+
MRO alone would decide which library implements a given method -- and *which
|
|
38
|
+
one wins changes with the interpreter version*, since stdlib `pathlib` keeps
|
|
39
|
+
gaining and changing methods. That produced version-dependent behavior in
|
|
40
|
+
both directions: CPython 3.14's new `copy()`/`move()` displaced ours (loudly
|
|
41
|
+
on non-local backends, silently and with different timestamp semantics on
|
|
42
|
+
local ones), while pre-3.12/3.13 stdlib lacked keywords our protocols
|
|
43
|
+
promise (`exists(follow_symlinks=)`, `read_text`/`write_text`'s `newline=`).
|
|
44
|
+
|
|
45
|
+
`pathlib_next.Path.__init_subclass__` therefore re-asserts the pathlib_next
|
|
46
|
+
implementation of `copy`, `move`, `exists`, `rglob`, `read_text` and
|
|
47
|
+
`write_text` for any subclass that would otherwise inherit stdlib's. This
|
|
48
|
+
applies automatically to downstream classes built with the documented
|
|
49
|
+
composition pattern (`class X(PosixPathname, Path)`), so implementers do not
|
|
50
|
+
have to hand-write forwarding methods.
|
|
51
|
+
|
|
52
|
+
Only stdlib `pathlib` is displaced: a subclass or mixin that defines one of
|
|
53
|
+
these operations itself always keeps its own implementation.
|
|
54
|
+
|
|
14
55
|
| Method | pathlib behavior | Our behavior | Why |
|
|
15
56
|
| --- | --- | --- | --- |
|
|
16
57
|
| `Uri("a").parent` | `PurePosixPath("a").parent == PurePosixPath(".")` | `Uri("a").parent` has path `""` (`Uri("")`, which round-trips) | `Uri` has no cwd-relative concept of `"."` -- an empty path is the URI-natural "no path" representation. Changing this would make `Uri("")` non-idempotent under `.parent`. |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pathlib_next"
|
|
7
|
-
version = "0.8.
|
|
7
|
+
version = "0.8.6"
|
|
8
8
|
authors = [{ name = "Jose A" }]
|
|
9
9
|
description = "Generic Path Protocol based pathlib"
|
|
10
10
|
readme = "README.md"
|
|
@@ -58,6 +58,15 @@ Homepage = "https://github.com/jose-pr/pathlib_next/"
|
|
|
58
58
|
Documentation = "https://jose-pr.github.io/pathlib_next/"
|
|
59
59
|
Issues = "https://github.com/jose-pr/pathlib_next/issues"
|
|
60
60
|
|
|
61
|
+
|
|
62
|
+
# Ship the consumer-facing docs inside the installed package, so they are
|
|
63
|
+
# readable from site-packages via importlib.resources without the repo.
|
|
64
|
+
# src/pathlib_next/AGENTS.md is included automatically by living in the package
|
|
65
|
+
# dir; the repo-root AGENTS.md is development-only and deliberately not
|
|
66
|
+
# shipped.
|
|
67
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
68
|
+
"README.md" = "pathlib_next/README.md"
|
|
69
|
+
|
|
61
70
|
[tool.hatch.build.targets.sdist]
|
|
62
71
|
exclude = ["/.*", "/benchmarks"]
|
|
63
72
|
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# `pathlib_next` — public API header
|
|
2
|
+
|
|
3
|
+
Header-file-style reference for the `pathlib_next` package: every public
|
|
4
|
+
export with its signature, arguments, contract, and gotchas, so this module
|
|
5
|
+
can be consumed without reading its source. Kept current with the public
|
|
6
|
+
API. For the project overview, install extras, and code layout, see the
|
|
7
|
+
<https://github.com/jose-pr/pathlib_next>. Any behavioral divergence from `pathlib.Path` is
|
|
8
|
+
recorded in `docs/divergences.md` — this file documents the *contract*, not
|
|
9
|
+
every internal deviation.
|
|
10
|
+
|
|
11
|
+
`import pathlib_next` re-exports `path`, `fspath`, `utils.glob`,
|
|
12
|
+
`utils.sync`, and (if `uritools` is importable) `uri.Uri`/`uri.UriPath`; a
|
|
13
|
+
missing `uritools` degrades that last import silently (`try`/`except
|
|
14
|
+
ImportError: pass`), so `pathlib_next.uri` may need an explicit
|
|
15
|
+
`from pathlib_next.uri import UriPath` even after a plain `import
|
|
16
|
+
pathlib_next`.
|
|
17
|
+
|
|
18
|
+
## Pure-path / I/O base (`pathlib_next.path`)
|
|
19
|
+
|
|
20
|
+
- **`Pathname`** — ABC for a pure (no I/O) path: `name`, `suffix`,
|
|
21
|
+
`suffixes`, `stem`, `segments` (abstract), `parts` (abstract),
|
|
22
|
+
`with_segments(*segments)` (abstract), `with_name`/`with_stem`/
|
|
23
|
+
`with_suffix`, `relative_to(other)`, `is_relative_to(other)`,
|
|
24
|
+
`__truediv__`/`joinpath`, `root`/`drive`/`anchor` (all `""` unless
|
|
25
|
+
overridden), `parent`/`parents` (abstract `parent`), `is_absolute()`
|
|
26
|
+
(abstract), `match(pattern, *, case_sensitive=None)`,
|
|
27
|
+
`full_match(pattern, *, case_sensitive=None)`, `as_posix()`,
|
|
28
|
+
`has_glob_pattern()`. `as_uri()` is abstract on `Pathname` itself.
|
|
29
|
+
- **`Path(Pathname, Chmod, Stat, BinaryOpen)`** — base class for I/O paths.
|
|
30
|
+
`Path(*args)` (the bare class, not a subclass) always constructs a
|
|
31
|
+
`LocalPath` (`fspath.py`) — the real local filesystem. Adds:
|
|
32
|
+
- `is_hidden()` — name starts with `"."`.
|
|
33
|
+
- `samefile(other_path)` — compares `(st_dev, st_ino)` from `stat()`;
|
|
34
|
+
raises `NotImplementedError` if either isn't available (`LocalPath` gets
|
|
35
|
+
a real implementation from `pathlib.Path` via MRO instead).
|
|
36
|
+
- `iterdir() -> Iterator[Self]` — **not implemented** by default (raises
|
|
37
|
+
`NotImplementedError`); every concrete `Path` overrides it.
|
|
38
|
+
- `_scandir() -> Iterator[tuple[str, FileStat | None]]` — default falls
|
|
39
|
+
back to `iterdir()` + one `stat()` per child; override directly when the
|
|
40
|
+
listing call already returns metadata (used by `walk()`/`glob()` so
|
|
41
|
+
remote schemes avoid a stat round trip per entry).
|
|
42
|
+
- `glob(pattern, *, case_sensitive=None, include_hidden=False,
|
|
43
|
+
recursive=None, dironly=None)` — a `"**"` pattern component
|
|
44
|
+
auto-enables recursion (pathlib parity); pass `recursive=False`
|
|
45
|
+
explicitly to disable it even with `"**"` present, or `True` to force it
|
|
46
|
+
without `"**"`. A recursive glob on a remote scheme walks the whole
|
|
47
|
+
subtree, one round trip per directory.
|
|
48
|
+
- `rglob(pattern, ...)` — `glob(f"**/{pattern}", recursive=True)`.
|
|
49
|
+
- `walk(top_down=True, on_error=None, follow_symlinks=False)` — drives
|
|
50
|
+
`_scandir()`, not `iterdir()`; the pre-seeded stat from `_scandir()` is
|
|
51
|
+
trusted only when `follow_symlinks=False` (its own default) — an
|
|
52
|
+
explicit `follow_symlinks=True` always re-`stat()`s each entry.
|
|
53
|
+
- `touch(mode=0o666, exist_ok=True)` — raises `FileExistsError` (not a
|
|
54
|
+
silent truncate) when `exist_ok=False` and the file exists.
|
|
55
|
+
- `_mkdir(mode)` (not implemented by default) / `mkdir(mode=0o777,
|
|
56
|
+
parents=False, exist_ok=False)` — `mkdir()` retries through
|
|
57
|
+
`_mkdir()`, creating parents on `FileNotFoundError` when `parents=True`.
|
|
58
|
+
- `unlink(missing_ok=False)` / `rmdir()` — not implemented by default;
|
|
59
|
+
every concrete `Path` overrides them.
|
|
60
|
+
- `rm(recursive=False, missing_ok=False, ignore_error=False |
|
|
61
|
+
Callable[[Exception, Self], bool])` — extension, no direct pathlib
|
|
62
|
+
equivalent. Removes a file or (with `recursive=True`) a directory tree;
|
|
63
|
+
`ignore_error` (bool or predicate) controls whether an error during the
|
|
64
|
+
walk is swallowed (predicate return `True`) or re-raised.
|
|
65
|
+
- `rename(target)` — not implemented by default.
|
|
66
|
+
- `copy(target, *, overwrite=False, follow_symlinks=True,
|
|
67
|
+
preserve_metadata=True, recursive=False, ignore_error=None)` —
|
|
68
|
+
`follow_symlinks`/`preserve_metadata` names match CPython 3.14's
|
|
69
|
+
`Path.copy()`; `overwrite` is this library's own extension (3.14 always
|
|
70
|
+
raises if the destination exists). `preserve_metadata` defaults `True`
|
|
71
|
+
here (3.14 defaults `False`) and only preserves `st_mode`, not
|
|
72
|
+
timestamps/xattrs. `ignore_error`, when given, receives exceptions
|
|
73
|
+
instead of raising (same contract as `rm()`'s callable form); `None`
|
|
74
|
+
(default) fails on the first error.
|
|
75
|
+
- `move(target, *, overwrite=False)` — tries `rename()` first, falls back
|
|
76
|
+
to `copy(recursive=True)` + `rm(recursive=True)`/`unlink()` when
|
|
77
|
+
`rename()` raises `NotImplementedError`.
|
|
78
|
+
- **`PathLike`** — `Union[str, Path]`. **`PurePathLike`** — `Union[str,
|
|
79
|
+
Pathname]`. **`FsPathLike`** — `Protocol` requiring `__fspath__() -> str`.
|
|
80
|
+
|
|
81
|
+
## Local filesystem (`pathlib_next.fspath`)
|
|
82
|
+
|
|
83
|
+
- **`LocalPath`** — `pathlib.WindowsPath`/`PosixPath` (by `os.name`) with
|
|
84
|
+
this library's `Path` mixed in via MRO. Behaves exactly like
|
|
85
|
+
`pathlib.Path` for anything not explicitly overridden (see
|
|
86
|
+
`docs/divergences.md`); overrides `_scandir()`, `walk()`, `copy()`,
|
|
87
|
+
`move()`, `stat()`, `chmod()`, and `glob()` to keep this project's
|
|
88
|
+
contracts (tuple-yielding `_scandir`, extended copy/move kwargs,
|
|
89
|
+
`follow_symlinks=` support pre-3.10) regardless of what a given Python
|
|
90
|
+
version's own `pathlib.Path` does at the same MRO position.
|
|
91
|
+
Stdlib inheritance is intentionally local-only: `MemPath`, `Uri`, and
|
|
92
|
+
`UriPath` implement the pathlib_next bases but are not stdlib
|
|
93
|
+
`PurePath`/`Path` instances because stdlib construction and operations
|
|
94
|
+
assume OS path syntax and a local filesystem. Conversely, a plain stdlib
|
|
95
|
+
`pathlib.Path` is not a `pathlib_next.Path`.
|
|
96
|
+
- **`PosixPathname`** / **`WindowsPathname`** — pure (no I/O) path classes
|
|
97
|
+
implementing `Pathname` on top of `pathlib.PurePosixPath`/
|
|
98
|
+
`PureWindowsPath`.
|
|
99
|
+
|
|
100
|
+
## In-memory filesystem (`pathlib_next.mempath`)
|
|
101
|
+
|
|
102
|
+
- **`MemPath(Path)`** — `MemPath(*segments, backend=None, **kwargs)`.
|
|
103
|
+
In-memory path over nested dicts; a `dict` value is a directory, a
|
|
104
|
+
`bytearray` value is a file's content. Reference exemplar for subclassing
|
|
105
|
+
`Path` directly. `relative_to()` is not implemented. `as_uri()` returns
|
|
106
|
+
`mempath:<url-quoted posix path>`. Supports `_open()` modes `"r"`, `"w"`,
|
|
107
|
+
`"x"`, `"a"` (the `"a"` extension isn't part of the base `BinaryOpen`
|
|
108
|
+
contract). `rename()` is not implemented (see the scheme feature matrix in
|
|
109
|
+
the README).
|
|
110
|
+
- **`MemPathBackend(dict)`** — the nested-dict storage. Share one instance
|
|
111
|
+
across `MemPath`s via `backend=` to give them the same virtual filesystem;
|
|
112
|
+
omitted, each root `MemPath()` gets its own.
|
|
113
|
+
|
|
114
|
+
## Protocols (`pathlib_next.protocols`)
|
|
115
|
+
|
|
116
|
+
- **`fs.FileStatLike`** — `Protocol`: `st_mode`, `st_size`, `st_mtime`
|
|
117
|
+
(all abstract properties).
|
|
118
|
+
- **`fs.Stat`** — `Protocol`. `stat(*, follow_symlinks=True) ->
|
|
119
|
+
FileStatLike` (not implemented by default). Derives `lstat()`,
|
|
120
|
+
`exists()`, `is_dir()`, `is_file()`, `is_symlink()`, `is_block_device()`,
|
|
121
|
+
`is_char_device()`, `is_fifo()`, `is_socket()` — all methods, not
|
|
122
|
+
properties. `exists()`/the `is_*` methods swallow `OSError`/`ValueError`
|
|
123
|
+
from `stat()` and report `False` rather than propagating (pathlib parity).
|
|
124
|
+
- **`fs.Chmod`** — `Protocol`. `chmod(mode, *, follow_symlinks=True)` (not
|
|
125
|
+
implemented by default); derives `lchmod(mode)`.
|
|
126
|
+
- **`io.BinaryOpen`** — `Protocol`. `_open(mode="r", buffering=-1) ->
|
|
127
|
+
io.IOBase` (not implemented by default; must yield a **binary** stream).
|
|
128
|
+
Derives `open(mode="r", buffering=-1, encoding=None, errors=None,
|
|
129
|
+
newline=None)`, `read_bytes()`, `read_text(encoding=None, errors=None,
|
|
130
|
+
newline=None)`, `write_bytes(data)`, `write_text(data, encoding=None,
|
|
131
|
+
errors=None, newline=None)`, `copy(target)` (streams this object's binary
|
|
132
|
+
content into another `BinaryOpen`).
|
|
133
|
+
|
|
134
|
+
## URIs (`pathlib_next.uri`)
|
|
135
|
+
|
|
136
|
+
Only importable if `uritools` is installed (the `uri` extra or any scheme
|
|
137
|
+
extra that depends on it).
|
|
138
|
+
|
|
139
|
+
- **`Uri(Pathname)`** — a pure (no I/O), RFC 3986 URI, lazily parsed into
|
|
140
|
+
`source`/`path`/`query`/`fragment` on first access. `Uri(*uris,
|
|
141
|
+
**options)` — multiple constructor args are joined pathlib-`joinpath`-style
|
|
142
|
+
(right to left, stopping at the first absolute segment) — this is **not**
|
|
143
|
+
RFC 3986 reference resolution, and `..` is never resolved during join (see
|
|
144
|
+
`docs/divergences.md`). Properties: `source -> Source`, `path -> str`,
|
|
145
|
+
`query -> str`, `fragment -> str`, `parts -> (source, path, query,
|
|
146
|
+
fragment)`, `normalized_path` (posixpath-normalized `path`), `segments`,
|
|
147
|
+
`suffix`, `stem`, `parent`. Methods: `as_uri(sanitize=False)` (sanitize
|
|
148
|
+
strips password from userinfo before formatting), `with_source(source)`,
|
|
149
|
+
`with_segments(*segments)`, `with_path(path)`, `with_query(query)`,
|
|
150
|
+
`with_fragment(fragment)`, `is_absolute()`, `is_relative_to(other)`,
|
|
151
|
+
`relative_to(other, *, walk_up=False)`, `is_local()` (delegates to
|
|
152
|
+
`Source.is_local()` — does a DNS lookup, cached per `Source`),
|
|
153
|
+
`as_posix()` (`user@host:path` / `host:path` form when a source is
|
|
154
|
+
present). `__fspath__()` only succeeds for a `file:`-scheme URI pointing
|
|
155
|
+
at this machine; otherwise raises `NotImplementedError`.
|
|
156
|
+
- **`UriPath(Uri, Path)`** — `Uri` + `Path` (I/O) + scheme dispatch.
|
|
157
|
+
`UriPath(*uris, **options)` (the bare class) parses the URI and returns an
|
|
158
|
+
instance of the concrete subclass registered for its scheme via
|
|
159
|
+
`__SCHEMES` (name-mangled per class — declare `__SCHEMES = ("http",
|
|
160
|
+
"https")` in the subclass body, not as a module-level or dynamically
|
|
161
|
+
assigned attribute, and never give a `__SCHEMES`-registered class a
|
|
162
|
+
leading underscore in its name, or the name-mangled lookup silently
|
|
163
|
+
misses). If the scheme isn't loaded yet, resolution tries a
|
|
164
|
+
`pathlib_next.schemes` entry point first, then imports the matching
|
|
165
|
+
builtin `uri/schemes/*` module — importing any module that defines a
|
|
166
|
+
`UriPath` subclass registers it. `backend` property — per-instance
|
|
167
|
+
connection/session state, lazily created via `_initbackend()` (override
|
|
168
|
+
in a scheme subclass; base returns `None`); `with_backend(backend)`
|
|
169
|
+
returns a new instance sharing the given backend. `_listdir() ->
|
|
170
|
+
Iterator[str]` (not implemented by default) / `_scandir()` (derives from
|
|
171
|
+
`_listdir()` + one `stat()` per child unless overridden directly — prefer
|
|
172
|
+
overriding `_scandir()` when the listing call already returns
|
|
173
|
+
type/size/mtime metadata, e.g. WebDAV PROPFIND, FTP MLSD, SFTP
|
|
174
|
+
`listdir_attr`, an S3 list page). `iterdir()` is provided (drives
|
|
175
|
+
`_scandir()`); implement `_listdir()` or `_scandir()`, not `iterdir()`
|
|
176
|
+
itself.
|
|
177
|
+
- **`Source`** (`uri.source`, re-exported at `uri.Source` via `uri/__init__`
|
|
178
|
+
imports) — `NamedTuple(scheme, userinfo, host, port)`; falsy when every
|
|
179
|
+
field is empty/`None`. `Source.from_str(source, strict=True) -> Source`
|
|
180
|
+
(`strict=True` raises `ValueError` if `source` carries a path/query/
|
|
181
|
+
fragment). `parsed_userinfo() -> (user, password)`. `get_scheme_cls(
|
|
182
|
+
schemesmap=None) -> type[UriPath]` — resolves (and lazily loads) the
|
|
183
|
+
scheme class. `is_local()` — DNS lookup, `lru_cache(maxsize=256)`d per
|
|
184
|
+
`Source` value; never call on a hot path uncached.
|
|
185
|
+
- **`Query(str)`** (`uri.query`) — a URI query string, buildable from a
|
|
186
|
+
`str`, a sequence of `(key, value)` pairs, or a mapping (`value` may be a
|
|
187
|
+
sequence to repeat the key). `Query(query, *, encoding="utf-8",
|
|
188
|
+
separator="&")`. `decode() -> list[tuple[str, str | None]]`,
|
|
189
|
+
`__iter__()` (iterates decoded pairs), `to_dict(*, single=False) ->
|
|
190
|
+
dict[str, list[str | None]]` (or `dict[str, str | None]` when
|
|
191
|
+
`single=True`, last value wins).
|
|
192
|
+
|
|
193
|
+
Built-in scheme modules live under `uri/schemes/` — see the table in the
|
|
194
|
+
<https://github.com/jose-pr/pathlib_next>. `PATHLIB_NEXT_SFTP_BACKEND` env var (`"paramiko"` /
|
|
195
|
+
`"asyncssh"` / `"auto"`, default `"auto"`) selects the `sftp:` backend;
|
|
196
|
+
precedence is an explicit class attribute > this env var > auto-detect
|
|
197
|
+
(prefers asyncssh if importable). `gs:` honors `STORAGE_EMULATOR_HOST` (set
|
|
198
|
+
into `os.environ` for the `google-cloud-storage` client, e.g. for a local
|
|
199
|
+
emulator) when configured on the path/backend.
|
|
200
|
+
|
|
201
|
+
## Testing helpers (`pathlib_next.testing`)
|
|
202
|
+
|
|
203
|
+
Not imported by `pathlib_next/__init__.py` (needs `pytest`, a test-only
|
|
204
|
+
dependency) — import explicitly: `from pathlib_next.testing import
|
|
205
|
+
PathContract`.
|
|
206
|
+
|
|
207
|
+
- **`PurePathContract`** — pure-path tests (name/suffix/stem, parent/
|
|
208
|
+
parents, joinpath/`/`, match). Requires only a `root` fixture.
|
|
209
|
+
- **`ReadPathContract(PurePathContract)`** — read-only I/O tests (exists/
|
|
210
|
+
is_dir/is_file, read_text/read_bytes, iterdir, stat). `root` fixture must
|
|
211
|
+
point at a directory pre-populated with the standard fixture tree
|
|
212
|
+
(`a.txt`, `b.py`, `.hidden.txt`, `sub/c.py`, `sub/nested/d.py`,
|
|
213
|
+
`empty_dir/`).
|
|
214
|
+
- **`PathContract(ReadPathContract)`** — full read/write contract (mkdir,
|
|
215
|
+
write_text/write_bytes, unlink, rmdir, rm(recursive=True), copy, move,
|
|
216
|
+
touch(exist_ok=False), mkdir(parents=True)). `root` fixture must be
|
|
217
|
+
writable.
|
|
218
|
+
|
|
219
|
+
Subclass one of these with your own `root` fixture to verify a custom
|
|
220
|
+
`Path`/`UriPath` implementation against the shared contract.
|
|
221
|
+
|
|
222
|
+
## Utilities (`pathlib_next.utils`)
|
|
223
|
+
|
|
224
|
+
- **`glob.glob(path, *, dironly=False, root_dir=None, recursive=False,
|
|
225
|
+
include_hidden=False, case_sensitive=None) -> Iterable[path-like]`** — the
|
|
226
|
+
engine behind `Path.glob()`/`rglob()`; works over anything exposing
|
|
227
|
+
`iterdir()`/`is_dir()`/`name`/`parents`/`has_glob_pattern()`. Dotfiles are
|
|
228
|
+
excluded from `*`/`?` matches unless `include_hidden=True`.
|
|
229
|
+
**`glob.full_match(segments, pattern, case_sensitive) -> bool`** —
|
|
230
|
+
pathlib 3.13 `full_match()` semantics, `"**"` matches zero or more
|
|
231
|
+
segments. **`glob.RECURSIVE`** = `"**"`.
|
|
232
|
+
- **`sync.PathSyncer(checksum=None, /, remove_missing=False,
|
|
233
|
+
follow_symlinks=True, hook=None, ignore_error=False)`** — one-way
|
|
234
|
+
checksum-driven tree sync between any two `Path` implementations.
|
|
235
|
+
`checksum` defaults to `utils.checksum.md5`. `.sync(source, target, /,
|
|
236
|
+
dry_run=False, ignore_error=False)` copies/creates in `target` whatever
|
|
237
|
+
differs from `source`; `remove_missing=True` also removes `target`
|
|
238
|
+
entries absent from `source`. `hook`/`.log()`/subclassing `.log()` are the
|
|
239
|
+
progress/logging seams; `SyncEvent` enum names the events fired.
|
|
240
|
+
**`sync.PathAndStat`** — a `Path` + cached `stat()` (`None` if missing);
|
|
241
|
+
`is_*` attribute access delegates to the cached stat, returning a
|
|
242
|
+
false-returning callable when the path doesn't exist.
|
|
243
|
+
- **`stat.FileStat(FileStatLike)`** — `FileStat(st_mode=None, st_size=0,
|
|
244
|
+
st_mtime=0, is_dir=False)`, slotted, for backends without a real
|
|
245
|
+
`os.stat_result` (`MemPath`, `HttpPath`, ...). `FileStat.from_stat(stat)`
|
|
246
|
+
copies recognized fields from any stat-like object (passes an existing
|
|
247
|
+
`FileStat` through unchanged). `FileStat.from_path(path, *,
|
|
248
|
+
follow_symlink=True) -> FileStat | None` (`None` on `FileNotFoundError`).
|
|
249
|
+
`is_dir()`/`is_file()`/etc. are **methods**, not properties — `if
|
|
250
|
+
st.is_dir` (no parens) is always truthy.
|
|
251
|
+
- **`checksum.md5(path, chunk_size=65536) -> str`** /
|
|
252
|
+
**`checksum.sha256(path, chunk_size=65536) -> str`** — streaming file
|
|
253
|
+
checksums over any `Path`.
|
|
254
|
+
- **`archive.make_archive(src, format, target)`** (`format` is `"zip"` or
|
|
255
|
+
`"tar"`) / **`archive.unpack_archive(archive, dest)`** (format
|
|
256
|
+
auto-detected from `archive.name`, falling back to magic-byte sniffing) —
|
|
257
|
+
stream-first, so `src`/`target`/`archive`/`dest` can be any `Path`
|
|
258
|
+
implementation, not just local files.
|
|
259
|
+
- **`LRU(func, maxsize=128)`** — thread-safe memoizing cache wrapping
|
|
260
|
+
`func`, itself callable; `.invalidate(*args)` evicts and recomputes one
|
|
261
|
+
entry; `.maxsize` is a settable property that evicts down to the new size.
|
|
262
|
+
- **`notimplemented(method)`** — decorator marking a protocol method;
|
|
263
|
+
raises `NotImplementedError` naming the method when called. Callers that
|
|
264
|
+
want a graceful fallback catch `NotImplementedError` (e.g. `move()` falls
|
|
265
|
+
back to copy+unlink when `rename` isn't implemented).
|
|
266
|
+
- **`sizeof_fmt(num) -> str`** — human-readable byte size (`"1.5K"`, ...).
|
|
267
|
+
**`parsedate(date) -> float`** — epoch seconds from a `str`/
|
|
268
|
+
`time.struct_time`/`tuple`/`float`; unparseable or `None` input returns
|
|
269
|
+
`0`, not "now". **`get_machine_ips() -> list[IPv4Address | IPv6Address]`**
|
|
270
|
+
— `lru_cache(maxsize=1)`d.
|
|
@@ -115,6 +115,36 @@ class LocalPath(
|
|
|
115
115
|
self, top_down=top_down, on_error=on_error, follow_symlinks=follow_symlinks
|
|
116
116
|
)
|
|
117
117
|
|
|
118
|
+
def copy(
|
|
119
|
+
self,
|
|
120
|
+
target,
|
|
121
|
+
*,
|
|
122
|
+
overwrite=False,
|
|
123
|
+
follow_symlinks=True,
|
|
124
|
+
preserve_metadata=True,
|
|
125
|
+
recursive=False,
|
|
126
|
+
ignore_error=None,
|
|
127
|
+
):
|
|
128
|
+
# Python 3.14 added pathlib.Path.copy(), which sits ahead of our
|
|
129
|
+
# generic implementation in the MRO and does not accept pathlib_next's
|
|
130
|
+
# overwrite=/recursive=/ignore_error= extensions. Keep LocalPath's
|
|
131
|
+
# cross-version contract stable by routing explicitly to our method.
|
|
132
|
+
return _proto.Path.copy(
|
|
133
|
+
self,
|
|
134
|
+
target,
|
|
135
|
+
overwrite=overwrite,
|
|
136
|
+
follow_symlinks=follow_symlinks,
|
|
137
|
+
preserve_metadata=preserve_metadata,
|
|
138
|
+
recursive=recursive,
|
|
139
|
+
ignore_error=ignore_error,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def move(self, target, *, overwrite=False):
|
|
143
|
+
# Python 3.14 added pathlib.Path.move() alongside copy(); route around
|
|
144
|
+
# the same MRO collision so overwrite= and the generic fallback remain
|
|
145
|
+
# available on every supported Python version.
|
|
146
|
+
return _proto.Path.move(self, target, overwrite=overwrite)
|
|
147
|
+
|
|
118
148
|
def stat(self, *, follow_symlinks=True):
|
|
119
149
|
# pathlib.Path.stat() (next in MRO via WindowsPath/PosixPath) only
|
|
120
150
|
# accepts follow_symlinks= on 3.10+; below that, lstat() is the
|
|
@@ -129,9 +159,7 @@ class LocalPath(
|
|
|
129
159
|
# platforms without os.lchmod, e.g. Windows).
|
|
130
160
|
if _HAS_FOLLOW_SYMLINKS:
|
|
131
161
|
return super().chmod(mode, follow_symlinks=follow_symlinks)
|
|
132
|
-
return (
|
|
133
|
-
super().chmod(mode) if follow_symlinks else super().lchmod(mode)
|
|
134
|
-
)
|
|
162
|
+
return super().chmod(mode) if follow_symlinks else super().lchmod(mode)
|
|
135
163
|
|
|
136
164
|
def glob(
|
|
137
165
|
self,
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
import abc as _abc
|
|
11
11
|
import os as _os
|
|
12
12
|
import re as _re
|
|
13
|
+
import sys as _sys
|
|
13
14
|
import typing as _ty
|
|
14
15
|
|
|
15
16
|
from . import utils as _utils
|
|
@@ -160,7 +161,11 @@ class Pathname(FsPathLike, _ty.Generic[_P]):
|
|
|
160
161
|
def with_suffix(self, suffix: str) -> _ty.Self:
|
|
161
162
|
"""Return a new path with the suffix changed or added."""
|
|
162
163
|
name = self.name
|
|
163
|
-
if
|
|
164
|
+
if (
|
|
165
|
+
suffix
|
|
166
|
+
and not suffix.startswith(".")
|
|
167
|
+
or (suffix == "." and _sys.version_info < (3, 14))
|
|
168
|
+
):
|
|
164
169
|
raise ValueError("Invalid suffix %r" % (suffix))
|
|
165
170
|
if not name:
|
|
166
171
|
raise ValueError("%r has an empty name" % (self,))
|
|
@@ -265,11 +270,90 @@ class Pathname(FsPathLike, _ty.Generic[_P]):
|
|
|
265
270
|
PurePathLike = _ty.Union[str, Pathname]
|
|
266
271
|
|
|
267
272
|
|
|
273
|
+
# Operations whose implementation must come from pathlib_next even when a
|
|
274
|
+
# concrete `pathlib` class sits ahead of us in a subclass's MRO. See
|
|
275
|
+
# `Path.__init_subclass__` for why this is needed and how it is applied.
|
|
276
|
+
#
|
|
277
|
+
# Two different, opposite failure modes motivate this list:
|
|
278
|
+
# * NEW stdlib overriding us: `copy`/`move` landed in CPython 3.14 and
|
|
279
|
+
# expect the private `_copy_from` protocol, so a downstream
|
|
280
|
+
# `class X(PosixPathname, Path)` (or any class mixing a concrete
|
|
281
|
+
# `pathlib` path) either crashes with
|
|
282
|
+
# `AttributeError: ... has no attribute '_copy_from'` on non-local
|
|
283
|
+
# backends, or -- worse -- SILENTLY succeeds on local-backed classes
|
|
284
|
+
# with stdlib's different metadata semantics (it preserves timestamps,
|
|
285
|
+
# ours preserves st_mode only). That makes mtime-based syncs converge
|
|
286
|
+
# on 3.14 and never converge on <=3.13.
|
|
287
|
+
# * OLD stdlib lacking our keywords: `exists(follow_symlinks=)` is 3.12+
|
|
288
|
+
# and `read_text`/`write_text`'s `newline=` is 3.13+ in CPython, and
|
|
289
|
+
# `rglob`'s `include_hidden=`/`recursive=`/`dironly=` extensions never
|
|
290
|
+
# existed there, so on the 3.9 floor the stdlib implementation rejects
|
|
291
|
+
# keywords this library's protocols promise.
|
|
292
|
+
#
|
|
293
|
+
# `glob`/`walk`/`_scandir` are deliberately absent: `LocalPath` overrides
|
|
294
|
+
# them itself (with local-specific behavior that must be kept), so they are
|
|
295
|
+
# already pathlib_next-owned wherever it matters.
|
|
296
|
+
_OPERATION_NAMES = (
|
|
297
|
+
"copy",
|
|
298
|
+
"move",
|
|
299
|
+
"exists",
|
|
300
|
+
"rglob",
|
|
301
|
+
"read_text",
|
|
302
|
+
"write_text",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
268
306
|
class Path(Pathname, Chmod, Stat, BinaryOpen):
|
|
269
307
|
"""Base class for manipulating paths with I/O."""
|
|
270
308
|
|
|
271
309
|
__slots__ = ()
|
|
272
310
|
|
|
311
|
+
def __init_subclass__(cls, **kwargs):
|
|
312
|
+
"""Guarantee pathlib_next operation precedence in every subclass.
|
|
313
|
+
|
|
314
|
+
Concrete path classes are routinely built by mixing a `pathlib`
|
|
315
|
+
class with this one -- our own `LocalPath` does it, and the
|
|
316
|
+
documented downstream recipe (`class X(PosixPathname, Path)`) does
|
|
317
|
+
it transitively. Python's MRO then resolves a name to whichever
|
|
318
|
+
base declares it first, which for those classes can be `pathlib`
|
|
319
|
+
rather than `pathlib_next` -- and which one wins changes with the
|
|
320
|
+
interpreter version, because stdlib `pathlib` keeps gaining and
|
|
321
|
+
changing methods (see `_OPERATION_NAMES`).
|
|
322
|
+
|
|
323
|
+
Rather than making every downstream implementer rediscover this and
|
|
324
|
+
hand-write forwarding methods, re-assert our implementations here
|
|
325
|
+
for any subclass that would otherwise inherit a non-pathlib_next
|
|
326
|
+
one. A subclass (or an intermediate mixin) that defines the method
|
|
327
|
+
*itself* is always left alone -- this only displaces implementations
|
|
328
|
+
coming from outside this library.
|
|
329
|
+
"""
|
|
330
|
+
super().__init_subclass__(**kwargs)
|
|
331
|
+
for name in _OPERATION_NAMES:
|
|
332
|
+
# A class that defines the operation in its OWN body is always
|
|
333
|
+
# authoritative -- never displace a deliberate override (this
|
|
334
|
+
# also covers `LocalPath.copy`/`move`'s explicit routing).
|
|
335
|
+
if name in vars(cls):
|
|
336
|
+
continue
|
|
337
|
+
# Find which class in the MRO actually supplies the inherited
|
|
338
|
+
# implementation. Checking the resolved function's `__module__`
|
|
339
|
+
# is not enough: a downstream mixin may legitimately define the
|
|
340
|
+
# method in its own module, and that must be honored too.
|
|
341
|
+
owner = next((base for base in cls.__mro__[1:] if name in vars(base)), None)
|
|
342
|
+
if owner is None:
|
|
343
|
+
continue
|
|
344
|
+
# Only stdlib `pathlib` is displaced. Anything else -- a
|
|
345
|
+
# downstream mixin, a user base class, or pathlib_next itself --
|
|
346
|
+
# is a deliberate implementation and is left alone. Matching on
|
|
347
|
+
# stdlib specifically (rather than "not pathlib_next") is what
|
|
348
|
+
# keeps this guard from hijacking third-party code.
|
|
349
|
+
owner_module = getattr(owner, "__module__", "") or ""
|
|
350
|
+
if owner_module != "pathlib" and not owner_module.startswith("pathlib."):
|
|
351
|
+
continue
|
|
352
|
+
ours = getattr(Path, name, None)
|
|
353
|
+
if ours is None:
|
|
354
|
+
continue
|
|
355
|
+
setattr(cls, name, ours)
|
|
356
|
+
|
|
273
357
|
def __new__(cls, *args, **kwargs):
|
|
274
358
|
if cls is Path:
|
|
275
359
|
from .fspath import LocalPath
|
|
@@ -528,9 +612,10 @@ class Path(Pathname, Chmod, Stat, BinaryOpen):
|
|
|
528
612
|
ignore_error: bool | _ty.Callable[[Exception, _ty.Self], bool] = False,
|
|
529
613
|
):
|
|
530
614
|
"""Remove this file or directory, optionally recursively and ignoring errors."""
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
615
|
+
# Same bool-or-callable normalization as copy()/PathSyncer, via the
|
|
616
|
+
# shared helper. A supplied callable keeps rm()'s own `(error, path)`
|
|
617
|
+
# arity -- arities differ per call site by design, see the helper.
|
|
618
|
+
_onerror = _utils.as_error_handler(ignore_error)
|
|
534
619
|
|
|
535
620
|
def _handle(error, path):
|
|
536
621
|
if not _onerror(error, path):
|
|
@@ -617,9 +702,14 @@ class Path(Pathname, Chmod, Stat, BinaryOpen):
|
|
|
617
702
|
(unlike 3.14's False) to match this method's pre-existing behavior
|
|
618
703
|
of always propagating st_mode; only st_mode is preserved, not
|
|
619
704
|
timestamps/xattrs -- full metadata preservation is not implemented.
|
|
620
|
-
`ignore_error`
|
|
621
|
-
|
|
622
|
-
(default)
|
|
705
|
+
`ignore_error` accepts a bool or a callable, matching `Path.rm()`'s
|
|
706
|
+
bool-or-callable contract. `True` ignores every error; `False` and
|
|
707
|
+
`None` (the default) fail on the first error. A callable is invoked
|
|
708
|
+
as `ignore_error(error)` -- this call site's own arity -- and, as it
|
|
709
|
+
always has here, is a *notification* hook: the error is suppressed
|
|
710
|
+
regardless of what it returns, so handlers like `errors.append`
|
|
711
|
+
(returning None) keep working. Only errors from *child* copies
|
|
712
|
+
during a `recursive=True` copy are routed here.
|
|
623
713
|
"""
|
|
624
714
|
if isinstance(target, str):
|
|
625
715
|
target = type(self)(target)
|
|
@@ -644,9 +734,15 @@ class Path(Pathname, Chmod, Stat, BinaryOpen):
|
|
|
644
734
|
ignore_error=ignore_error,
|
|
645
735
|
)
|
|
646
736
|
except Exception as e:
|
|
647
|
-
|
|
737
|
+
# A callable stays a notify-and-suppress hook (its return
|
|
738
|
+
# value was never consulted here, and callers such as
|
|
739
|
+
# `errors.append` rely on that). Bools are new: True
|
|
740
|
+
# suppresses, False/None raise -- matching rm()'s bool
|
|
741
|
+
# semantics without changing the callable contract.
|
|
742
|
+
if callable(ignore_error):
|
|
743
|
+
ignore_error(e)
|
|
744
|
+
elif not ignore_error:
|
|
648
745
|
raise
|
|
649
|
-
ignore_error(e)
|
|
650
746
|
return
|
|
651
747
|
|
|
652
748
|
if target.exists():
|
|
@@ -693,4 +789,3 @@ class Path(Pathname, Chmod, Stat, BinaryOpen):
|
|
|
693
789
|
|
|
694
790
|
|
|
695
791
|
PathLike = _ty.Union[str, Path]
|
|
696
|
-
|