pathlib-next 0.4.1__tar.gz → 0.6.0__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.4.1 → pathlib_next-0.6.0}/.gitignore +3 -0
- pathlib_next-0.6.0/CHANGELOG.md +243 -0
- pathlib_next-0.6.0/PKG-INFO +203 -0
- pathlib_next-0.6.0/README.md +168 -0
- pathlib_next-0.6.0/docs/divergences.md +61 -0
- pathlib_next-0.6.0/docs/guides/extending.md +121 -0
- pathlib_next-0.6.0/docs/guides/schemes.md +62 -0
- pathlib_next-0.6.0/docs/index.md +85 -0
- pathlib_next-0.6.0/examples/http_listing.py +38 -0
- pathlib_next-0.6.0/examples/local_and_mem.py +63 -0
- pathlib_next-0.6.0/examples/sftp_sync.py +57 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/mkdocs.yml +4 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/pyproject.toml +2 -2
- pathlib_next-0.6.0/src/pathlib_next/fspath.py +135 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/mempath.py +67 -17
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/path.py +148 -25
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/protocols/fs.py +10 -5
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/protocols/io.py +13 -4
- pathlib_next-0.6.0/src/pathlib_next/testing.py +132 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/__init__.py +64 -13
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/query.py +41 -2
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/schemes/__init__.py +4 -0
- pathlib_next-0.6.0/src/pathlib_next/uri/schemes/archive.py +266 -0
- pathlib_next-0.6.0/src/pathlib_next/uri/schemes/data.py +58 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/schemes/file.py +7 -0
- pathlib_next-0.6.0/src/pathlib_next/uri/schemes/ftp.py +205 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/schemes/http.py +34 -6
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/schemes/sftp.py +26 -4
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/uri/source.py +11 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/utils/__init__.py +13 -5
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/utils/glob.py +42 -4
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/utils/stat.py +5 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/utils/sync.py +15 -0
- pathlib_next-0.6.0/tests/conftest.py +63 -0
- pathlib_next-0.6.0/tests/test_archive_uri.py +198 -0
- pathlib_next-0.6.0/tests/test_contract.py +29 -0
- pathlib_next-0.6.0/tests/test_data_uri.py +76 -0
- pathlib_next-0.6.0/tests/test_ftp.py +313 -0
- pathlib_next-0.6.0/tests/test_glob.py +91 -0
- pathlib_next-0.6.0/tests/test_http.py +70 -0
- pathlib_next-0.6.0/tests/test_mempath.py +155 -0
- pathlib_next-0.6.0/tests/test_parity_io.py +159 -0
- pathlib_next-0.6.0/tests/test_parity_pure.py +102 -0
- pathlib_next-0.6.0/tests/test_pathname.py +136 -0
- pathlib_next-0.6.0/tests/test_query.py +45 -0
- pathlib_next-0.6.0/tests/test_sftp.py +158 -0
- pathlib_next-0.6.0/tests/test_smoke.py +95 -0
- pathlib_next-0.6.0/tests/test_source.py +62 -0
- pathlib_next-0.6.0/tests/test_sync.py +122 -0
- pathlib_next-0.4.1/tests/test_uri.py → pathlib_next-0.6.0/tests/test_uri_parse.py +47 -0
- pathlib_next-0.6.0/tests/test_uri_path.py +100 -0
- pathlib_next-0.6.0/tests/test_utils.py +68 -0
- pathlib_next-0.4.1/CHANGELOG.md +0 -39
- pathlib_next-0.4.1/PKG-INFO +0 -113
- pathlib_next-0.4.1/README.md +0 -79
- pathlib_next-0.4.1/docs/index.md +0 -29
- pathlib_next-0.4.1/examples/example.py +0 -70
- pathlib_next-0.4.1/src/pathlib_next/fspath.py +0 -85
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/LICENSE +0 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/docs/api/reference.md +0 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/docs/changelog.md +0 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/__init__.py +0 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/protocols/__init__.py +0 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/src/pathlib_next/py.typed +0 -0
- {pathlib_next-0.4.1 → pathlib_next-0.6.0}/tests/test_local.py +0 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.6.0] - 2026-07-11
|
|
11
|
+
|
|
12
|
+
### Added (Phase 7a new schemes, stdlib-only, no new deps)
|
|
13
|
+
- `data:` scheme (RFC 2397, `pathlib_next.uri.schemes.data.DataUri`):
|
|
14
|
+
read-only, no backend/connection -- the entire file content is embedded
|
|
15
|
+
in the URI (`data:[<mediatype>][;base64],<data>`). `stat().st_size` is
|
|
16
|
+
the decoded payload length; `iterdir()` raises `NotADirectoryError`
|
|
17
|
+
(it's always a single file); write operations raise `NotImplementedError`.
|
|
18
|
+
- `ftp:`/`ftps:` scheme (`pathlib_next.uri.schemes.ftp.FtpPath`): full
|
|
19
|
+
read/write/list access via stdlib `ftplib`, with a thread-keyed LRU
|
|
20
|
+
connection cache mirroring `sftp.py`. Listing/stat prefer MLSD (RFC
|
|
21
|
+
3659); servers without it fall back to NLST (listing) and SIZE
|
|
22
|
+
(file-only stat). Writes buffer in memory and upload via STOR/APPE on
|
|
23
|
+
`close()`. `chmod()` uses the common but non-standard `SITE CHMOD`
|
|
24
|
+
extension (may not be supported by every server).
|
|
25
|
+
- `zip:`/`tar:` archive paths (`pathlib_next.uri.schemes.archive`):
|
|
26
|
+
`<scheme>:<archive-uri>!/<inner-path>` (Java-style `!/` separator,
|
|
27
|
+
URI form proposed to and confirmed by the user before implementation).
|
|
28
|
+
The archive half is itself any absolute URI with an explicit scheme, so
|
|
29
|
+
archives are readable straight off any other backend (`file:`, `http:`,
|
|
30
|
+
`sftp:`, `ftp:`, `data:`, ...). Read is supported for both schemes.
|
|
31
|
+
Write is `zip:`-only, and only for brand-new entries in a local (`file:`)
|
|
32
|
+
outer archive (overwriting/deleting/renaming an existing entry would
|
|
33
|
+
need a full-archive rewrite -- not implemented, raises
|
|
34
|
+
`NotImplementedError`). `tar:` auto-detects `.tar.gz`/`.tar.bz2`/`.tar.xz`
|
|
35
|
+
compression and is always read-only.
|
|
36
|
+
|
|
37
|
+
## [0.5.0] - 2026-07-11
|
|
38
|
+
|
|
39
|
+
### Fixed (critical -- found while writing Phase 6 examples)
|
|
40
|
+
- `Path("...")` -- the top-level dispatcher documented in this project's
|
|
41
|
+
own README quick start and used throughout -- silently dropped its
|
|
42
|
+
constructor arguments on Python <3.12, leaving a blank instance that
|
|
43
|
+
crashed with `AttributeError: _drv` the moment anything touched it (e.g.
|
|
44
|
+
the `/` operator). Masked on 3.12+, where the real parsing happens in
|
|
45
|
+
`__init__` (called separately, with the original args, regardless of what
|
|
46
|
+
`__new__` did) rather than `__new__` itself. Every one of Phase 5's 300
|
|
47
|
+
tests constructed via `LocalPath(...)` directly instead, so this went
|
|
48
|
+
undetected until `examples/local_and_mem.py` exercised the documented
|
|
49
|
+
`Path(...)` entry point end to end.
|
|
50
|
+
|
|
51
|
+
### Fixed (found by the new Phase 5 test suite, not in the original bug list)
|
|
52
|
+
- `LocalPath.stat()`/`chmod()` inherit directly from `pathlib.Path` via MRO
|
|
53
|
+
and crashed with `TypeError` on Python 3.9 the moment anything passed
|
|
54
|
+
`follow_symlinks=` (e.g. `Path.walk()`'s default `follow_symlinks=False`)
|
|
55
|
+
-- now shimmed with `lstat()`/`lchmod()` on <3.10, same as the existing
|
|
56
|
+
`FileUri` shim (which now just delegates to `LocalPath`).
|
|
57
|
+
- `MemPath.__init__` decided whether to propagate a parent's backend with
|
|
58
|
+
`if _backend and backend is None:` -- an empty (but valid) backend dict is
|
|
59
|
+
falsy, so joining off a freshly-created, empty `MemPath` silently gave the
|
|
60
|
+
child a disconnected new backend instead of sharing the parent's.
|
|
61
|
+
- `MemPath.stat()` never set `st_size` for files (always defaulted to `0`),
|
|
62
|
+
breaking any size-based checksum comparison (notably `PathSyncer`'s
|
|
63
|
+
typical usage).
|
|
64
|
+
- `glob()`'s core algorithm decided whether to recurse into the *parent*
|
|
65
|
+
directory using whether the *leaf* segment is a wildcard, instead of
|
|
66
|
+
whether the *parent path itself* contains one. Since a wildcarded leaf
|
|
67
|
+
with a literal parent directory is the overwhelmingly common case
|
|
68
|
+
(`glob("*.py")`), this always took the "recurse into parent" branch,
|
|
69
|
+
which only degenerated back to the correct single directory when the
|
|
70
|
+
parent has a non-empty literal name to re-match against -- true for
|
|
71
|
+
essentially every real filesystem path except an OS root. It silently
|
|
72
|
+
returned the wrong result on `MemPath`'s virtual root (empty name).
|
|
73
|
+
- `HttpPath.iterdir()` gave every subdirectory entry an empty `.name`:
|
|
74
|
+
directory-listing entries for subdirectories carry a trailing `/`
|
|
75
|
+
(`htmllistparse`'s convention), which wasn't stripped before building the
|
|
76
|
+
child's path, and `Pathname.name` derives from the last path segment --
|
|
77
|
+
empty for a trailing-slash path.
|
|
78
|
+
- `SftpPath.rename()` resolved a plain string target relative to `self`
|
|
79
|
+
(joining it as a child, e.g. `"/a.txt".rename("b.txt")` produced
|
|
80
|
+
`"/a.txt/b.txt"`) instead of `self`'s parent (sibling rename).
|
|
81
|
+
|
|
82
|
+
### Added (test suite)
|
|
83
|
+
- Full pytest suite (`tests/`): pure-path parity against `pathlib.PurePosixPath`
|
|
84
|
+
(`test_parity_pure.py`), local I/O parity against `pathlib.Path`/`os.walk`
|
|
85
|
+
(`test_parity_io.py`), a reusable filesystem-contract mixin run against
|
|
86
|
+
`LocalPath`/`MemPath`/`FileUri` and exported as `pathlib_next.testing.
|
|
87
|
+
PathContract` for third-party `Path`/`UriPath` implementers
|
|
88
|
+
(`test_contract.py`), glob vs. stdlib ground truth (`test_glob.py`), URI
|
|
89
|
+
parsing/scheme-dispatch/query/source coverage, MemPath- and SFTP-specific
|
|
90
|
+
unit tests (SFTP mocked, no real server), HTTP tests against a real stdlib
|
|
91
|
+
`ThreadingHTTPServer`, and `PathSyncer` coverage. 300 tests, ~85% line
|
|
92
|
+
coverage, green on both Python 3.9 and 3.13.
|
|
93
|
+
|
|
94
|
+
### Added (docs)
|
|
95
|
+
- `docs/guides/schemes.md` (capability matrix per scheme) and
|
|
96
|
+
`docs/guides/extending.md` (both extension tracks, with worked examples
|
|
97
|
+
and `pathlib_next.testing.PathContract` usage). Rewrote `docs/index.md`
|
|
98
|
+
and the README with a 30-second example per scheme and a capability
|
|
99
|
+
matrix. Class-level docstrings added across the package for the rendered
|
|
100
|
+
API reference.
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
- `examples/example.py` (an unstructured scratch script) split into three
|
|
104
|
+
focused, runnable examples: `examples/local_and_mem.py` (self-contained,
|
|
105
|
+
no network), `examples/http_listing.py` and `examples/sftp_sync.py`
|
|
106
|
+
(network-touching, guarded under `if __name__ == "__main__"`,
|
|
107
|
+
configurable via env vars, fail soft when unreachable/unconfigured).
|
|
108
|
+
|
|
109
|
+
### Added
|
|
110
|
+
- `Pathname.joinpath()`, `Pathname.full_match()` (3.13 parity, supports `**`
|
|
111
|
+
matching any number of segments), `Pathname.anchor`/`drive`/`root`
|
|
112
|
+
(generic derivation for non-local paths), `Path.rglob()`,
|
|
113
|
+
`read_text(..., newline=)` (3.13 parity), `Path.samefile()` (default
|
|
114
|
+
`st_dev`/`st_ino` comparison when the backend's `stat()` provides them,
|
|
115
|
+
`NotImplementedError` otherwise).
|
|
116
|
+
- `Path.glob()`/`LocalPath.glob()`: `recursive=` now auto-detects (`True` if
|
|
117
|
+
the pattern has a `"**"` component) instead of defaulting to `False`;
|
|
118
|
+
explicit `recursive=True`/`False` still overrides.
|
|
119
|
+
- `Path.copy()`: raises `IsADirectoryError` when the target is an existing
|
|
120
|
+
directory (previously misbehaved); gained `follow_symlinks=`/
|
|
121
|
+
`preserve_metadata=` kwargs, named to match CPython 3.14's `Path.copy()`.
|
|
122
|
+
- `docs/divergences.md`: registry of every deliberate behavioral divergence
|
|
123
|
+
from `pathlib`, with rationale. Linked from the docs nav.
|
|
124
|
+
|
|
125
|
+
### Fixed
|
|
126
|
+
- `Path.mkdir(parents=True)` created intermediate parents with `exist_ok=False`
|
|
127
|
+
(racy, and wrong when a parent already existed) and dropped the caller's
|
|
128
|
+
`exist_ok` on the final retry.
|
|
129
|
+
- `Path.touch(exist_ok=False)` silently truncated an existing file instead of
|
|
130
|
+
raising `FileExistsError` (pathlib parity).
|
|
131
|
+
- `LocalPath.glob()`'s `dironly` parameter defaulted to `False`, which made the
|
|
132
|
+
`is None` check for trailing-slash directory-only detection dead code.
|
|
133
|
+
- `Stat._st_mode()` only caught `FileNotFoundError`, letting `PermissionError`
|
|
134
|
+
and other `OSError`s propagate out of `exists()`/`is_dir()`/etc. where pathlib
|
|
135
|
+
returns `False`. Also fixed: `follow_symlinks` was accepted but never
|
|
136
|
+
forwarded to the underlying `stat()` call, so `is_symlink()` never actually
|
|
137
|
+
inspected the symlink itself.
|
|
138
|
+
- `MemPath._open()` treated any mode other than `"w"` as a read, so `"a"`/`"x"`
|
|
139
|
+
silently misbehaved; now dispatches `r`/`w`/`x`/`a` correctly and raises
|
|
140
|
+
`NotImplementedError` for anything else. `MemBytesIO.close()` used
|
|
141
|
+
`seek(0);read()` instead of `getvalue()`, losing content if the caller's
|
|
142
|
+
cursor wasn't already at position 0 when closing.
|
|
143
|
+
- `MemPath.normalized` mangled `".."`-escaping paths (e.g. `".."`) into `"."`;
|
|
144
|
+
now normalizes against a virtual root so they clamp at the root instead.
|
|
145
|
+
- `PathAndStat.__getattr__()` returned `None` for any unrecognized attribute
|
|
146
|
+
instead of raising `AttributeError`, breaking `hasattr()`-based logic.
|
|
147
|
+
- `parsedate(None)` / an unparseable date string returned "now" instead of
|
|
148
|
+
epoch 0, which could poison `PathSyncer`'s checksum/freshness comparisons for
|
|
149
|
+
HTTP sources with no `Last-Modified` header.
|
|
150
|
+
- `HttpPath.stat()` used a bare `except:`; cached `_isdir` from a response that
|
|
151
|
+
hadn't been confirmed successful yet (including 404s); and didn't fall back
|
|
152
|
+
to GET when a server rejected `HEAD` with 405.
|
|
153
|
+
- `uri.Query` no longer depends on `uritools`' private `_querydict`/`_querylist`
|
|
154
|
+
helpers (reimplemented locally against the public `uriencode()`).
|
|
155
|
+
- `Uri` join (`_load_parts`): `query`/`fragment` are now resolved with the same
|
|
156
|
+
"last segment that actually sets one wins" rule already used for `source`
|
|
157
|
+
(previously any segment, even one with no query/fragment, would blank out an
|
|
158
|
+
earlier segment's). Join semantics are now documented explicitly:
|
|
159
|
+
pathlib-`joinpath`-like, not RFC 3986 reference resolution, `..` is never
|
|
160
|
+
resolved during join.
|
|
161
|
+
- `Source.is_local()` (DNS lookup) and `get_machine_ips()` are now
|
|
162
|
+
`functools.lru_cache`d -- previously ran on every call.
|
|
163
|
+
|
|
164
|
+
### Fixed (crash-level bugs)
|
|
165
|
+
- `MemPath.stat()`/`MemPath._open()` returned a `FileNotFoundError` instance instead
|
|
166
|
+
of raising it for a missing path, causing an unrelated `AttributeError` downstream.
|
|
167
|
+
- `LRU.invalidate()` called `self.lock()` instead of using `self.lock` as a context
|
|
168
|
+
manager (`RLock` isn't callable) -- broke the SFTP client reconnect path.
|
|
169
|
+
- `Pathname.match()` had reversed `isinstance()` arguments and compared against
|
|
170
|
+
`str(self)` (which includes scheme/host for `Uri`) instead of `as_posix()`.
|
|
171
|
+
- Glob wildcard detection (`WILCARD_PATTERN`, renamed `WILDCARD_PATTERN`, old name
|
|
172
|
+
kept as an alias) used `.match()` (anchored) instead of `.search()`, so patterns
|
|
173
|
+
like `"foo*"` weren't recognized as wildcards.
|
|
174
|
+
- `Uri` was unhashable (defined `__eq__` without `__hash__`); `__eq__` now also
|
|
175
|
+
returns `NotImplemented` for non-`Pathname`/`str` operands instead of raising.
|
|
176
|
+
- `Uri.is_relative_to()` used `str.startswith()` on normalized path strings, so
|
|
177
|
+
`/foo/bar2` was incorrectly reported as relative to `/foo/bar`; now compares
|
|
178
|
+
path segments.
|
|
179
|
+
- `Uri.relative_to(walk_up=True)` was dead code -- an early guard raised
|
|
180
|
+
`ValueError` before the walk-up loop ever ran.
|
|
181
|
+
- `HttpPath.is_dir()`/`is_file()` tested truthiness of bound methods
|
|
182
|
+
(`self._is_dir`, `self.is_dir`) instead of calling/checking the right attribute,
|
|
183
|
+
so both always returned truthy nonsense.
|
|
184
|
+
- `SftpPath.chmod()` didn't accept `follow_symlinks=`, so the inherited `lchmod()`
|
|
185
|
+
crashed with `TypeError`; now raises `NotImplementedError` for
|
|
186
|
+
`follow_symlinks=False` (paramiko has no `lchmod`).
|
|
187
|
+
- `SftpPath` defined `_rename()`, which nothing ever called -- renamed to
|
|
188
|
+
`rename()` so `move()`/`rename()` actually use SFTP's native rename instead of
|
|
189
|
+
silently falling back to copy+unlink for every move.
|
|
190
|
+
- `Uri.__init__()` used a bare `except:` around `Path.as_uri()` (now
|
|
191
|
+
`except ValueError:`, matching what `as_uri()` actually raises for relative
|
|
192
|
+
paths) and crashed with `AttributeError` when constructing from an
|
|
193
|
+
`os.PathLike` that only implements `__fspath__` (no `as_posix()`).
|
|
194
|
+
- `Path.rm(ignore_error=callable)` never actually called the callable -- both
|
|
195
|
+
branches of its error handler returned the callable object itself.
|
|
196
|
+
|
|
197
|
+
### Fixed (Python 3.9/3.10 compatibility)
|
|
198
|
+
- Actual Python 3.9/3.10 runtime compatibility (CI previously only tested 3.11/3.13
|
|
199
|
+
and missed these): `LocalPath`/`Uri` case-sensitivity and path-separator detection
|
|
200
|
+
crashed on 3.9-3.11 (`_flavour` object has no `normcase`); `open(mode="r")` crashed
|
|
201
|
+
on <3.10 (`io.text_encoding` is 3.10+); glob pattern compilation crashed on <3.11
|
|
202
|
+
(`re.NOFLAG` is 3.11+); `FileUri.stat()`/`chmod()` crashed on 3.9
|
|
203
|
+
(`pathlib.Path.stat/chmod` gained `follow_symlinks=` in 3.10; raises
|
|
204
|
+
`NotImplementedError` there for `follow_symlinks=False`).
|
|
205
|
+
- `LocalPath._path_separators` returned the env-var list separator (`;`/`:`) instead
|
|
206
|
+
of the path separator, and could include a `None` altsep on POSIX.
|
|
207
|
+
|
|
208
|
+
### Added
|
|
209
|
+
- `tests/test_smoke.py`: regression coverage for README/example snippets across
|
|
210
|
+
supported Python versions.
|
|
211
|
+
|
|
212
|
+
## [0.4.1] - 2026-07-11
|
|
213
|
+
|
|
214
|
+
### Fixed
|
|
215
|
+
- Removed explicit `[tool.hatch.build.targets.wheel]` packages config that caused hatchling to fail resolving `README.md` during editable installs on CI.
|
|
216
|
+
- Converted `README.md` from a symlink (mode `120000`) to a regular file, fixing `git checkout` failures on macOS and Windows runners.
|
|
217
|
+
- Removed agent-tooling references (`AGENTS.md`, `PYTHON.md`) from committed files.
|
|
218
|
+
|
|
219
|
+
## [0.4.0] - 2026-07-11
|
|
220
|
+
|
|
221
|
+
### Added
|
|
222
|
+
- Standardized repository layout and relocated examples to `examples/` directory.
|
|
223
|
+
- Configured MkDocs documentation site with dynamic API reference using `mkdocstrings`.
|
|
224
|
+
- Added GitHub Actions workflows for matrix testing (`test.yml`) and release pipelines (`release.yml`).
|
|
225
|
+
- Added typing marker `py.typed` for PEP 561 compliance.
|
|
226
|
+
|
|
227
|
+
### Changed
|
|
228
|
+
- Added backward compatibility support for Python 3.9 and 3.10: added `from __future__ import annotations` across the codebase, refactored runtime-evaluated union types to use `typing.Union`, and provided fallbacks for `TypeAlias` and `ParamSpec`.
|
|
229
|
+
- Updated package requirement to `requires-python = ">=3.9"`.
|
|
230
|
+
|
|
231
|
+
## [0.3.5] - 2026-07-11
|
|
232
|
+
|
|
233
|
+
### Added
|
|
234
|
+
- Split path into protocols that can be standalone.
|
|
235
|
+
- Sync error handling.
|
|
236
|
+
- Generic Path Protocol based pathlib implementation for URI paths with file access support for sftp, http, file schemes.
|
|
237
|
+
|
|
238
|
+
[Unreleased]: https://github.com/jose-pr/pathlib_next/compare/v0.6.0...HEAD
|
|
239
|
+
[0.6.0]: https://github.com/jose-pr/pathlib_next/compare/v0.5.0...v0.6.0
|
|
240
|
+
[0.5.0]: https://github.com/jose-pr/pathlib_next/compare/v0.4.1...v0.5.0
|
|
241
|
+
[0.4.1]: https://github.com/jose-pr/pathlib_next/compare/v0.4.0...v0.4.1
|
|
242
|
+
[0.4.0]: https://github.com/jose-pr/pathlib_next/releases/tag/v0.4.0
|
|
243
|
+
[0.3.5]: https://github.com/jose-pr/pathlib_next/releases/tag/v0.3.5
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pathlib_next
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Generic Path Protocol based pathlib
|
|
5
|
+
Project-URL: Homepage, https://github.com/jose-pr/pathlib_next/
|
|
6
|
+
Project-URL: Documentation, https://jose-pr.github.io/pathlib_next/
|
|
7
|
+
Project-URL: Issues, https://github.com/jose-pr/pathlib_next/issues
|
|
8
|
+
Author: Jose A
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: build; extra == 'dev'
|
|
16
|
+
Requires-Dist: hatchling; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
19
|
+
Requires-Dist: twine; extra == 'dev'
|
|
20
|
+
Provides-Extra: docs
|
|
21
|
+
Requires-Dist: mkdocs; extra == 'docs'
|
|
22
|
+
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
23
|
+
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
24
|
+
Provides-Extra: http
|
|
25
|
+
Requires-Dist: bs4; extra == 'http'
|
|
26
|
+
Requires-Dist: htmllistparse; extra == 'http'
|
|
27
|
+
Requires-Dist: requests; extra == 'http'
|
|
28
|
+
Requires-Dist: uritools; extra == 'http'
|
|
29
|
+
Provides-Extra: sftp
|
|
30
|
+
Requires-Dist: paramiko; extra == 'sftp'
|
|
31
|
+
Requires-Dist: uritools; extra == 'sftp'
|
|
32
|
+
Provides-Extra: uri
|
|
33
|
+
Requires-Dist: uritools; extra == 'uri'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# pathlib_next
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/pathlib_next/)
|
|
39
|
+
[](https://pypi.org/project/pathlib_next/)
|
|
40
|
+
[](LICENSE)
|
|
41
|
+
[](https://jose-pr.github.io/pathlib_next/)
|
|
42
|
+
[](https://github.com/jose-pr/pathlib_next/actions/workflows/test.yml)
|
|
43
|
+
|
|
44
|
+
A **robust, extensible pathlib-like base** for any resource addressable as a
|
|
45
|
+
path or URI. Same method names, signatures, semantics, and exception types as
|
|
46
|
+
`pathlib.Path` wherever a `pathlib.Path` equivalent exists -- write code once
|
|
47
|
+
against `Path`/`UriPath` and it works against your local disk, an in-memory
|
|
48
|
+
tree, an HTTP index, or an SFTP server. Every intentional divergence from
|
|
49
|
+
`pathlib`'s behavior is documented, not silent -- see
|
|
50
|
+
[`docs/divergences.md`](https://jose-pr.github.io/pathlib_next/divergences/).
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
| Capability | `LocalPath` | `file:` | `mem:` (`MemPath`) | `http(s):` | `sftp:` |
|
|
55
|
+
| --- | --- | --- | --- | --- | --- |
|
|
56
|
+
| Read | Yes | Yes | Yes | Yes | Yes |
|
|
57
|
+
| Write | Yes | Yes | Yes | No | Yes |
|
|
58
|
+
| List (`iterdir`) | Yes | Yes | Yes | Yes (HTML index) | Yes |
|
|
59
|
+
| Stat / exists / is_dir / is_file | Yes | Yes | Yes | Yes | Yes |
|
|
60
|
+
| `mkdir` | Yes | Yes | Yes | No | Yes |
|
|
61
|
+
| Delete | Yes | Yes | Yes | No | Yes |
|
|
62
|
+
| `rename` | Yes | Yes | No (copy+unlink fallback) | No | Yes |
|
|
63
|
+
| Extra required | none | none | none | `http` | `sftp` |
|
|
64
|
+
|
|
65
|
+
Every scheme shares the same `glob()`, `walk()`, `copy()`/`move()`, `rm()`
|
|
66
|
+
implementations -- see the full matrix and notes in
|
|
67
|
+
[Schemes](https://jose-pr.github.io/pathlib_next/guides/schemes/).
|
|
68
|
+
|
|
69
|
+
- **Unified path interface** across local files, in-memory paths, and
|
|
70
|
+
`sftp`/`http`/`file` URIs.
|
|
71
|
+
- **`MemPath`** -- a lightweight virtual filesystem for mocks, tests, or
|
|
72
|
+
transient storage.
|
|
73
|
+
- **`PathSyncer`** -- one-way checksum-driven tree sync between any two
|
|
74
|
+
`Path` implementations, with dry-run and event hooks.
|
|
75
|
+
- **`Query`/`Source`** -- parse and serialize URL query strings and URI
|
|
76
|
+
authority components.
|
|
77
|
+
- **Extensible two ways**: subclass `Path` directly for a custom
|
|
78
|
+
non-URI resource, or subclass `UriPath` for a new URI scheme -- see
|
|
79
|
+
[Extending](https://jose-pr.github.io/pathlib_next/guides/extending/).
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install pathlib_next
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Optional features/extras:
|
|
88
|
+
|
|
89
|
+
| Extra/flag | Adds | Needed for |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| `uri` | `uritools` | URI parsing capabilities |
|
|
92
|
+
| `http` | `requests`, `bs4`, `htmllistparse` | Read and list files over HTTP/HTTPS |
|
|
93
|
+
| `sftp` | `paramiko` | SFTP path operations and transfers |
|
|
94
|
+
|
|
95
|
+
`import pathlib_next` and `LocalPath`/`MemPath` work with no extras
|
|
96
|
+
installed.
|
|
97
|
+
|
|
98
|
+
## Quick start
|
|
99
|
+
|
|
100
|
+
**Local filesystem** -- drop-in `pathlib.Path`:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from pathlib_next import Path
|
|
104
|
+
|
|
105
|
+
p = Path("./data") / "report.txt"
|
|
106
|
+
p.write_text("hello")
|
|
107
|
+
print(p.read_text())
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**In-memory** (`mem:`) -- a virtual filesystem, no disk I/O:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from pathlib_next.mempath import MemPath
|
|
114
|
+
|
|
115
|
+
p = MemPath("/config/settings.json")
|
|
116
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
p.write_text('{"debug": true}')
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**`file:`** -- the same local filesystem, addressed as a URI:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from pathlib_next.uri import UriPath
|
|
124
|
+
|
|
125
|
+
p = UriPath("file:./data/report.txt")
|
|
126
|
+
print(p.read_text())
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**`http(s):`** -- read files and list Apache/nginx-style directory indexes:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from pathlib_next.uri import UriPath
|
|
133
|
+
|
|
134
|
+
p = UriPath("http://example.com/data/")
|
|
135
|
+
for child in p.iterdir():
|
|
136
|
+
if child.is_file():
|
|
137
|
+
print(child.name, child.stat().st_size)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**`sftp:`** -- same interface, over SSH:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from pathlib_next.uri import UriPath
|
|
144
|
+
|
|
145
|
+
p = UriPath("sftp://user@host/var/log/app.log")
|
|
146
|
+
print(p.read_text())
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Extending
|
|
150
|
+
|
|
151
|
+
Two first-class ways to add a new path-addressable resource -- both covered
|
|
152
|
+
in depth, with worked examples, in
|
|
153
|
+
[Extending](https://jose-pr.github.io/pathlib_next/guides/extending/):
|
|
154
|
+
|
|
155
|
+
- Subclass `Path` directly for a custom, non-URI resource (`MemPath` is the
|
|
156
|
+
reference exemplar).
|
|
157
|
+
- Subclass `UriPath` and set `__SCHEMES` for a new URI scheme (`FileUri`/
|
|
158
|
+
`HttpPath`/`SftpPath` are the built-in examples).
|
|
159
|
+
|
|
160
|
+
`pathlib_next.testing.PathContract` is a reusable pytest mixin covering the
|
|
161
|
+
baseline contract every implementation must satisfy -- subclass it with a
|
|
162
|
+
`root` fixture to verify your own.
|
|
163
|
+
|
|
164
|
+
## API overview
|
|
165
|
+
|
|
166
|
+
| Module/Package | Purpose |
|
|
167
|
+
| --- | --- |
|
|
168
|
+
| `pathlib_next.path` | Base Path implementation and protocols |
|
|
169
|
+
| `pathlib_next.uri` | URI/URL specific path support and Query utils |
|
|
170
|
+
| `pathlib_next.mempath` | In-memory transient path structure |
|
|
171
|
+
| `pathlib_next.utils.sync` | Synchronization functions and PathSyncer class |
|
|
172
|
+
| `pathlib_next.testing` | `PathContract`, a pytest mixin for verifying custom implementations |
|
|
173
|
+
|
|
174
|
+
## Supported Python versions
|
|
175
|
+
|
|
176
|
+
Python >= 3.9, tested on 3.9 and 3.13 in CI (see
|
|
177
|
+
[`.github/workflows/test.yml`](.github/workflows/test.yml)).
|
|
178
|
+
|
|
179
|
+
## Development
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
pip install -e ".[dev,uri,http,sftp]"
|
|
183
|
+
pytest -q
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
If you maintain separate virtual environments per Python version locally
|
|
187
|
+
(e.g. `.venv/3.9/`, `.venv/3.13/`), run the same `pytest -q` in each --
|
|
188
|
+
CI does the equivalent across Python 3.9/3.13 on Linux, macOS, and Windows.
|
|
189
|
+
|
|
190
|
+
### Releasing
|
|
191
|
+
|
|
192
|
+
This project follows [Semantic Versioning](https://semver.org/) and keeps a
|
|
193
|
+
[`CHANGELOG.md`](CHANGELOG.md). Pushing a tag matching `v*` triggers the release
|
|
194
|
+
workflow: test gate → build → publish → docs deploy.
|
|
195
|
+
|
|
196
|
+
### Documentation site
|
|
197
|
+
|
|
198
|
+
MkDocs builds the API reference from `docs/`, published on every
|
|
199
|
+
release. To preview locally: `mkdocs serve`.
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# pathlib_next
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pathlib_next/)
|
|
4
|
+
[](https://pypi.org/project/pathlib_next/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://jose-pr.github.io/pathlib_next/)
|
|
7
|
+
[](https://github.com/jose-pr/pathlib_next/actions/workflows/test.yml)
|
|
8
|
+
|
|
9
|
+
A **robust, extensible pathlib-like base** for any resource addressable as a
|
|
10
|
+
path or URI. Same method names, signatures, semantics, and exception types as
|
|
11
|
+
`pathlib.Path` wherever a `pathlib.Path` equivalent exists -- write code once
|
|
12
|
+
against `Path`/`UriPath` and it works against your local disk, an in-memory
|
|
13
|
+
tree, an HTTP index, or an SFTP server. Every intentional divergence from
|
|
14
|
+
`pathlib`'s behavior is documented, not silent -- see
|
|
15
|
+
[`docs/divergences.md`](https://jose-pr.github.io/pathlib_next/divergences/).
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
| Capability | `LocalPath` | `file:` | `mem:` (`MemPath`) | `http(s):` | `sftp:` |
|
|
20
|
+
| --- | --- | --- | --- | --- | --- |
|
|
21
|
+
| Read | Yes | Yes | Yes | Yes | Yes |
|
|
22
|
+
| Write | Yes | Yes | Yes | No | Yes |
|
|
23
|
+
| List (`iterdir`) | Yes | Yes | Yes | Yes (HTML index) | Yes |
|
|
24
|
+
| Stat / exists / is_dir / is_file | Yes | Yes | Yes | Yes | Yes |
|
|
25
|
+
| `mkdir` | Yes | Yes | Yes | No | Yes |
|
|
26
|
+
| Delete | Yes | Yes | Yes | No | Yes |
|
|
27
|
+
| `rename` | Yes | Yes | No (copy+unlink fallback) | No | Yes |
|
|
28
|
+
| Extra required | none | none | none | `http` | `sftp` |
|
|
29
|
+
|
|
30
|
+
Every scheme shares the same `glob()`, `walk()`, `copy()`/`move()`, `rm()`
|
|
31
|
+
implementations -- see the full matrix and notes in
|
|
32
|
+
[Schemes](https://jose-pr.github.io/pathlib_next/guides/schemes/).
|
|
33
|
+
|
|
34
|
+
- **Unified path interface** across local files, in-memory paths, and
|
|
35
|
+
`sftp`/`http`/`file` URIs.
|
|
36
|
+
- **`MemPath`** -- a lightweight virtual filesystem for mocks, tests, or
|
|
37
|
+
transient storage.
|
|
38
|
+
- **`PathSyncer`** -- one-way checksum-driven tree sync between any two
|
|
39
|
+
`Path` implementations, with dry-run and event hooks.
|
|
40
|
+
- **`Query`/`Source`** -- parse and serialize URL query strings and URI
|
|
41
|
+
authority components.
|
|
42
|
+
- **Extensible two ways**: subclass `Path` directly for a custom
|
|
43
|
+
non-URI resource, or subclass `UriPath` for a new URI scheme -- see
|
|
44
|
+
[Extending](https://jose-pr.github.io/pathlib_next/guides/extending/).
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install pathlib_next
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Optional features/extras:
|
|
53
|
+
|
|
54
|
+
| Extra/flag | Adds | Needed for |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `uri` | `uritools` | URI parsing capabilities |
|
|
57
|
+
| `http` | `requests`, `bs4`, `htmllistparse` | Read and list files over HTTP/HTTPS |
|
|
58
|
+
| `sftp` | `paramiko` | SFTP path operations and transfers |
|
|
59
|
+
|
|
60
|
+
`import pathlib_next` and `LocalPath`/`MemPath` work with no extras
|
|
61
|
+
installed.
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
**Local filesystem** -- drop-in `pathlib.Path`:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from pathlib_next import Path
|
|
69
|
+
|
|
70
|
+
p = Path("./data") / "report.txt"
|
|
71
|
+
p.write_text("hello")
|
|
72
|
+
print(p.read_text())
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**In-memory** (`mem:`) -- a virtual filesystem, no disk I/O:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from pathlib_next.mempath import MemPath
|
|
79
|
+
|
|
80
|
+
p = MemPath("/config/settings.json")
|
|
81
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
p.write_text('{"debug": true}')
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**`file:`** -- the same local filesystem, addressed as a URI:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from pathlib_next.uri import UriPath
|
|
89
|
+
|
|
90
|
+
p = UriPath("file:./data/report.txt")
|
|
91
|
+
print(p.read_text())
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**`http(s):`** -- read files and list Apache/nginx-style directory indexes:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from pathlib_next.uri import UriPath
|
|
98
|
+
|
|
99
|
+
p = UriPath("http://example.com/data/")
|
|
100
|
+
for child in p.iterdir():
|
|
101
|
+
if child.is_file():
|
|
102
|
+
print(child.name, child.stat().st_size)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**`sftp:`** -- same interface, over SSH:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from pathlib_next.uri import UriPath
|
|
109
|
+
|
|
110
|
+
p = UriPath("sftp://user@host/var/log/app.log")
|
|
111
|
+
print(p.read_text())
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Extending
|
|
115
|
+
|
|
116
|
+
Two first-class ways to add a new path-addressable resource -- both covered
|
|
117
|
+
in depth, with worked examples, in
|
|
118
|
+
[Extending](https://jose-pr.github.io/pathlib_next/guides/extending/):
|
|
119
|
+
|
|
120
|
+
- Subclass `Path` directly for a custom, non-URI resource (`MemPath` is the
|
|
121
|
+
reference exemplar).
|
|
122
|
+
- Subclass `UriPath` and set `__SCHEMES` for a new URI scheme (`FileUri`/
|
|
123
|
+
`HttpPath`/`SftpPath` are the built-in examples).
|
|
124
|
+
|
|
125
|
+
`pathlib_next.testing.PathContract` is a reusable pytest mixin covering the
|
|
126
|
+
baseline contract every implementation must satisfy -- subclass it with a
|
|
127
|
+
`root` fixture to verify your own.
|
|
128
|
+
|
|
129
|
+
## API overview
|
|
130
|
+
|
|
131
|
+
| Module/Package | Purpose |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| `pathlib_next.path` | Base Path implementation and protocols |
|
|
134
|
+
| `pathlib_next.uri` | URI/URL specific path support and Query utils |
|
|
135
|
+
| `pathlib_next.mempath` | In-memory transient path structure |
|
|
136
|
+
| `pathlib_next.utils.sync` | Synchronization functions and PathSyncer class |
|
|
137
|
+
| `pathlib_next.testing` | `PathContract`, a pytest mixin for verifying custom implementations |
|
|
138
|
+
|
|
139
|
+
## Supported Python versions
|
|
140
|
+
|
|
141
|
+
Python >= 3.9, tested on 3.9 and 3.13 in CI (see
|
|
142
|
+
[`.github/workflows/test.yml`](.github/workflows/test.yml)).
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pip install -e ".[dev,uri,http,sftp]"
|
|
148
|
+
pytest -q
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
If you maintain separate virtual environments per Python version locally
|
|
152
|
+
(e.g. `.venv/3.9/`, `.venv/3.13/`), run the same `pytest -q` in each --
|
|
153
|
+
CI does the equivalent across Python 3.9/3.13 on Linux, macOS, and Windows.
|
|
154
|
+
|
|
155
|
+
### Releasing
|
|
156
|
+
|
|
157
|
+
This project follows [Semantic Versioning](https://semver.org/) and keeps a
|
|
158
|
+
[`CHANGELOG.md`](CHANGELOG.md). Pushing a tag matching `v*` triggers the release
|
|
159
|
+
workflow: test gate → build → publish → docs deploy.
|
|
160
|
+
|
|
161
|
+
### Documentation site
|
|
162
|
+
|
|
163
|
+
MkDocs builds the API reference from `docs/`, published on every
|
|
164
|
+
release. To preview locally: `mkdocs serve`.
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT — see [LICENSE](LICENSE).
|