pathlib-next 0.8.4__tar.gz → 0.8.5__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 (111) hide show
  1. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/.gitignore +2 -2
  2. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/CHANGELOG.md +21 -1
  3. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/PKG-INFO +1 -1
  4. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/docs/divergences.md +20 -0
  5. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/pyproject.toml +10 -1
  6. pathlib_next-0.8.5/src/pathlib_next/AGENTS.md +270 -0
  7. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/fspath.py +31 -3
  8. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/path.py +6 -2
  9. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_parity_io.py +11 -2
  10. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_pathname.py +13 -6
  11. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/LICENSE +0 -0
  12. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/README.md +0 -0
  13. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/docs/api/mempath.md +0 -0
  14. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/docs/api/path.md +0 -0
  15. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/docs/api/testing.md +0 -0
  16. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/docs/api/uri.md +0 -0
  17. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/docs/api/utils.md +0 -0
  18. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/docs/benchmarks.md +0 -0
  19. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/docs/changelog.md +0 -0
  20. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/docs/guides/cli.md +0 -0
  21. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/docs/guides/extending.md +0 -0
  22. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/docs/guides/schemes.md +0 -0
  23. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/docs/index.md +0 -0
  24. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/examples/az_listing.py +0 -0
  25. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/examples/data_and_archive.py +0 -0
  26. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/examples/ftp_listing.py +0 -0
  27. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/examples/github_listing.py +0 -0
  28. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/examples/gitlab_listing.py +0 -0
  29. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/examples/gs_listing.py +0 -0
  30. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/examples/http_listing.py +0 -0
  31. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/examples/local_and_mem.py +0 -0
  32. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/examples/s3_listing.py +0 -0
  33. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/examples/sftp_sync.py +0 -0
  34. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/examples/webdav_roundtrip.py +0 -0
  35. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/mkdocs.yml +0 -0
  36. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/__init__.py +0 -0
  37. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/mempath.py +0 -0
  38. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/protocols/__init__.py +0 -0
  39. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/protocols/fs.py +0 -0
  40. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/protocols/io.py +0 -0
  41. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/py.typed +0 -0
  42. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/testing.py +0 -0
  43. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/tools/__init__.py +0 -0
  44. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/tools/uripath.py +0 -0
  45. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/__init__.py +0 -0
  46. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/query.py +0 -0
  47. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/__init__.py +0 -0
  48. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/_gitrepo.py +0 -0
  49. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/archive/__init__.py +0 -0
  50. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/archive/_base.py +0 -0
  51. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/archive/tar.py +0 -0
  52. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/archive/zip.py +0 -0
  53. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/az.py +0 -0
  54. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/data.py +0 -0
  55. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/dav.py +0 -0
  56. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/file.py +0 -0
  57. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/ftp.py +0 -0
  58. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/git/__init__.py +0 -0
  59. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/git/_base.py +0 -0
  60. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/git/github.py +0 -0
  61. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/git/gitlab.py +0 -0
  62. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/github.py +0 -0
  63. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/gitlab.py +0 -0
  64. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/gs.py +0 -0
  65. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/http.py +0 -0
  66. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/s3.py +0 -0
  67. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/sftp/__init__.py +0 -0
  68. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/sftp/_asyncssh.py +0 -0
  69. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/sftp/_paramiko.py +0 -0
  70. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/schemes/sftp/_sshconfig.py +0 -0
  71. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/uri/source.py +0 -0
  72. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/utils/__init__.py +0 -0
  73. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/utils/archive.py +0 -0
  74. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/utils/checksum.py +0 -0
  75. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/utils/glob.py +0 -0
  76. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/utils/stat.py +0 -0
  77. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/src/pathlib_next/utils/sync.py +0 -0
  78. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/conftest.py +0 -0
  79. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_archive_uri.py +0 -0
  80. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_az.py +0 -0
  81. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_az_fake.py +0 -0
  82. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_contract.py +0 -0
  83. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_data_uri.py +0 -0
  84. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_dav.py +0 -0
  85. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_ftp.py +0 -0
  86. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_gitrepo.py +0 -0
  87. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_glob.py +0 -0
  88. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_gs.py +0 -0
  89. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_gs_fake.py +0 -0
  90. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_http.py +0 -0
  91. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_http_live.py +0 -0
  92. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_http_parser.py +0 -0
  93. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_local.py +0 -0
  94. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_mempath.py +0 -0
  95. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_parity_pure.py +0 -0
  96. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_path_gaps.py +0 -0
  97. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_plugins.py +0 -0
  98. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_properties.py +0 -0
  99. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_query.py +0 -0
  100. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_s3.py +0 -0
  101. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_sftp.py +0 -0
  102. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_sftp_asyncssh.py +0 -0
  103. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_smoke.py +0 -0
  104. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_source.py +0 -0
  105. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_sync.py +0 -0
  106. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_uri_parse.py +0 -0
  107. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_uri_path.py +0 -0
  108. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_uripath_tool.py +0 -0
  109. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_utils.py +0 -0
  110. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/tests/test_walk.py +0 -0
  111. {pathlib_next-0.8.4 → pathlib_next-0.8.5}/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,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.5] - 2026-07-26
11
+
12
+ ### Fixed
13
+ - **`LocalPath.copy()` and `LocalPath.move()` resolved to the incompatible
14
+ stdlib implementations on Python 3.14.** Python 3.14 added methods with
15
+ those names ahead of `pathlib_next.Path` in `LocalPath`'s MRO, so calls using
16
+ pathlib_next extensions such as `overwrite=` or `recursive=` failed with
17
+ `TypeError`. `LocalPath` now routes both methods explicitly through the
18
+ pathlib_next implementations on every supported Python version.
19
+ - Generic paths now follow Python 3.14's updated `PurePath.with_suffix(".")`
20
+ behavior while retaining the earlier `ValueError` behavior on older Python
21
+ versions.
22
+
23
+ ### Changed
24
+ - Documented that stdlib inheritance is deliberately local-only:
25
+ `LocalPath` is a real `pathlib.Path`, while URI, in-memory, and other virtual
26
+ implementations inherit the generic pathlib_next contracts without claiming
27
+ local-filesystem semantics.
28
+
10
29
  ## [0.8.4] - 2026-07-18
11
30
 
12
31
  ### Changed
@@ -516,7 +535,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
516
535
  - Sync error handling.
517
536
  - Generic Path Protocol based pathlib implementation for URI paths with file access support for sftp, http, file schemes.
518
537
 
519
- [Unreleased]: https://github.com/jose-pr/pathlib_next/compare/v0.8.4...HEAD
538
+ [Unreleased]: https://github.com/jose-pr/pathlib_next/compare/v0.8.5...HEAD
539
+ [0.8.5]: https://github.com/jose-pr/pathlib_next/compare/v0.8.4...v0.8.5
520
540
  [0.8.4]: https://github.com/jose-pr/pathlib_next/compare/v0.8.3...v0.8.4
521
541
  [0.8.3]: https://github.com/jose-pr/pathlib_next/compare/v0.8.2...v0.8.3
522
542
  [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.5
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,26 @@ 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
+
14
34
  | Method | pathlib behavior | Our behavior | Why |
15
35
  | --- | --- | --- | --- |
16
36
  | `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.5"
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,))
@@ -693,4 +698,3 @@ class Path(Pathname, Chmod, Stat, BinaryOpen):
693
698
 
694
699
 
695
700
  PathLike = _ty.Union[str, Path]
696
-
@@ -5,6 +5,7 @@ for the handful of methods LocalPath explicitly overrides (touch, mkdir,
5
5
  glob, rm/copy/move which have no direct pathlib.Path equivalent to diverge
6
6
  from pre-3.14).
7
7
  """
8
+
8
9
  import os
9
10
 
10
11
  import pytest
@@ -84,7 +85,11 @@ def test_glob_hidden_excluded_by_default(fixture_tree):
84
85
  def test_walk_matches_os_walk(fixture_tree):
85
86
  root = pathlib_next.LocalPath(fixture_tree)
86
87
  ours = sorted(
87
- (str(p.relative_to(root).as_posix() if p != root else "."), sorted(d), sorted(f))
88
+ (
89
+ str(p.relative_to(root).as_posix() if p != root else "."),
90
+ sorted(d),
91
+ sorted(f),
92
+ )
88
93
  for p, d, f in root.walk()
89
94
  )
90
95
  theirs = sorted(
@@ -201,6 +206,11 @@ def test_copy_recursive(tmp_path):
201
206
  assert (dst / "f1.txt").read_text() == "1-updated"
202
207
 
203
208
 
209
+ def test_local_copy_and_move_resolve_to_pathlib_next():
210
+ assert pathlib_next.LocalPath.copy.__module__.startswith("pathlib_next.")
211
+ assert pathlib_next.LocalPath.move.__module__.startswith("pathlib_next.")
212
+
213
+
204
214
  def test_move_recursive_fallback(tmp_path):
205
215
  root = pathlib_next.LocalPath(tmp_path)
206
216
  src = root / "src"
@@ -220,4 +230,3 @@ def test_move_recursive_fallback(tmp_path):
220
230
  assert not src.exists()
221
231
  assert (dst / "f1.txt").read_text() == "1"
222
232
  assert (dst / "sub" / "f2.txt").read_text() == "2"
223
-
@@ -4,6 +4,9 @@ Pathname (PosixPathname -- LocalPath itself is excluded here since pathlib's
4
4
  own PurePath wins those methods via MRO, see test_parity_pure.py instead),
5
5
  Uri, and MemPath.
6
6
  """
7
+
8
+ import pathlib
9
+
7
10
  import pytest
8
11
 
9
12
  from pathlib_next.fspath import PosixPathname
@@ -57,8 +60,13 @@ def test_with_suffix_invalid_raises(cls):
57
60
  p = cls("a/b.txt")
58
61
  with pytest.raises(ValueError):
59
62
  p.with_suffix("txt")
60
- with pytest.raises(ValueError):
61
- p.with_suffix(".")
63
+ try:
64
+ expected = pathlib.PurePosixPath("a/b.txt").with_suffix(".").name
65
+ except ValueError:
66
+ with pytest.raises(ValueError):
67
+ p.with_suffix(".")
68
+ else:
69
+ assert p.with_suffix(".").name == expected
62
70
 
63
71
 
64
72
  @pytest.mark.parametrize("cls", IMPLS)
@@ -88,7 +96,7 @@ def test_parent_and_parents(cls):
88
96
  parents = [pp.as_posix() for pp in p.parents]
89
97
  assert parents[:2] == ["a/b", "a"]
90
98
  assert len(parents) == 3 # trailing root/"." element, like pathlib
91
-
99
+
92
100
  # Slicing
93
101
  try:
94
102
  sliced = p.parents[0:2]
@@ -96,7 +104,7 @@ def test_parent_and_parents(cls):
96
104
  except TypeError:
97
105
  # Python 3.9 stdlib pathlib.PurePath.parents doesn't support slicing
98
106
  pass
99
-
107
+
100
108
  # Negative indexing
101
109
  try:
102
110
  assert p.parents[-1].as_posix() == "" or p.parents[-1].as_posix() == "."
@@ -105,7 +113,7 @@ def test_parent_and_parents(cls):
105
113
  except IndexError:
106
114
  # Python 3.9 stdlib pathlib.PurePath.parents doesn't support negative indexing
107
115
  pass
108
-
116
+
109
117
  # IndexError out of bounds
110
118
  with pytest.raises(IndexError):
111
119
  _ = p.parents[3]
@@ -113,7 +121,6 @@ def test_parent_and_parents(cls):
113
121
  _ = p.parents[-4]
114
122
 
115
123
 
116
-
117
124
  @pytest.mark.parametrize("cls", IMPLS)
118
125
  def test_has_glob_pattern(cls):
119
126
  assert cls("a/*.py").has_glob_pattern()
File without changes
File without changes
File without changes
File without changes