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.
Files changed (112) hide show
  1. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/.gitignore +2 -2
  2. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/CHANGELOG.md +67 -1
  3. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/PKG-INFO +1 -1
  4. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/divergences.md +41 -0
  5. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/pyproject.toml +10 -1
  6. pathlib_next-0.8.6/src/pathlib_next/AGENTS.md +270 -0
  7. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/fspath.py +31 -3
  8. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/path.py +105 -10
  9. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/utils/__init__.py +32 -1
  10. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/utils/sync.py +56 -14
  11. pathlib_next-0.8.6/tests/test_mro_precedence.py +250 -0
  12. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_parity_io.py +11 -2
  13. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_path_gaps.py +98 -9
  14. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_pathname.py +13 -6
  15. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_sync.py +96 -0
  16. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/LICENSE +0 -0
  17. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/README.md +0 -0
  18. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/api/mempath.md +0 -0
  19. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/api/path.md +0 -0
  20. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/api/testing.md +0 -0
  21. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/api/uri.md +0 -0
  22. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/api/utils.md +0 -0
  23. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/benchmarks.md +0 -0
  24. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/changelog.md +0 -0
  25. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/guides/cli.md +0 -0
  26. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/guides/extending.md +0 -0
  27. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/guides/schemes.md +0 -0
  28. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/docs/index.md +0 -0
  29. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/az_listing.py +0 -0
  30. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/data_and_archive.py +0 -0
  31. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/ftp_listing.py +0 -0
  32. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/github_listing.py +0 -0
  33. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/gitlab_listing.py +0 -0
  34. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/gs_listing.py +0 -0
  35. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/http_listing.py +0 -0
  36. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/local_and_mem.py +0 -0
  37. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/s3_listing.py +0 -0
  38. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/sftp_sync.py +0 -0
  39. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/examples/webdav_roundtrip.py +0 -0
  40. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/mkdocs.yml +0 -0
  41. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/__init__.py +0 -0
  42. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/mempath.py +0 -0
  43. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/protocols/__init__.py +0 -0
  44. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/protocols/fs.py +0 -0
  45. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/protocols/io.py +0 -0
  46. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/py.typed +0 -0
  47. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/testing.py +0 -0
  48. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/tools/__init__.py +0 -0
  49. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/tools/uripath.py +0 -0
  50. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/__init__.py +0 -0
  51. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/query.py +0 -0
  52. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/__init__.py +0 -0
  53. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/_gitrepo.py +0 -0
  54. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/archive/__init__.py +0 -0
  55. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/archive/_base.py +0 -0
  56. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/archive/tar.py +0 -0
  57. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/archive/zip.py +0 -0
  58. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/az.py +0 -0
  59. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/data.py +0 -0
  60. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/dav.py +0 -0
  61. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/file.py +0 -0
  62. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/ftp.py +0 -0
  63. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/git/__init__.py +0 -0
  64. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/git/_base.py +0 -0
  65. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/git/github.py +0 -0
  66. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/git/gitlab.py +0 -0
  67. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/github.py +0 -0
  68. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/gitlab.py +0 -0
  69. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/gs.py +0 -0
  70. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/http.py +0 -0
  71. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/s3.py +0 -0
  72. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/sftp/__init__.py +0 -0
  73. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/sftp/_asyncssh.py +0 -0
  74. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/sftp/_paramiko.py +0 -0
  75. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/schemes/sftp/_sshconfig.py +0 -0
  76. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/uri/source.py +0 -0
  77. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/utils/archive.py +0 -0
  78. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/utils/checksum.py +0 -0
  79. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/utils/glob.py +0 -0
  80. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/src/pathlib_next/utils/stat.py +0 -0
  81. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/conftest.py +0 -0
  82. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_archive_uri.py +0 -0
  83. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_az.py +0 -0
  84. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_az_fake.py +0 -0
  85. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_contract.py +0 -0
  86. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_data_uri.py +0 -0
  87. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_dav.py +0 -0
  88. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_ftp.py +0 -0
  89. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_gitrepo.py +0 -0
  90. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_glob.py +0 -0
  91. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_gs.py +0 -0
  92. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_gs_fake.py +0 -0
  93. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_http.py +0 -0
  94. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_http_live.py +0 -0
  95. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_http_parser.py +0 -0
  96. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_local.py +0 -0
  97. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_mempath.py +0 -0
  98. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_parity_pure.py +0 -0
  99. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_plugins.py +0 -0
  100. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_properties.py +0 -0
  101. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_query.py +0 -0
  102. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_s3.py +0 -0
  103. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_sftp.py +0 -0
  104. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_sftp_asyncssh.py +0 -0
  105. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_smoke.py +0 -0
  106. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_source.py +0 -0
  107. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_uri_parse.py +0 -0
  108. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_uri_path.py +0 -0
  109. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_uripath_tool.py +0 -0
  110. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_utils.py +0 -0
  111. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_walk.py +0 -0
  112. {pathlib_next-0.8.4 → pathlib_next-0.8.6}/tests/test_webdav.py +0 -0
@@ -4,8 +4,8 @@ build/
4
4
  site/
5
5
 
6
6
  # Agent/harness tooling
7
- AGENTS.md
8
- .agents/
7
+ .agents
8
+ *.local.md
9
9
  CLAUDE.md
10
10
  CLAUDE.local.md
11
11
  .claude/
@@ -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.4...HEAD
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pathlib_next
3
- Version: 0.8.4
3
+ Version: 0.8.6
4
4
  Summary: Generic Path Protocol based pathlib
5
5
  Project-URL: Homepage, https://github.com/jose-pr/pathlib_next/
6
6
  Project-URL: Documentation, https://jose-pr.github.io/pathlib_next/
@@ -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.4"
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 suffix and not suffix.startswith(".") or suffix == ".":
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
- _onerror = lambda _err, _path: (
532
- ignore_error(_err, _path) if callable(ignore_error) else bool(ignore_error)
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` is a callable matching `Path.rm()`'s contract: when
621
- provided, exceptions are passed to it instead of raised; when None
622
- (default), fail on the first error.
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
- if ignore_error is None:
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
-