Repository navigation
v0.9.7
Added
-
rm(follow_symlinks=, follow_binds=): what a recursive removal does
with a symlink, and with a binding (a Windows junction, a mount point).
Each takesFalse(the default -- remove the entry itself, never its
contents, asrm -rdoes),True(remove what is behind it),None
(leave it in place, so the enclosing directory is not empty and reports
it), or a callablepolicy(path) -> bool | Noneasked per entry, so one
tree can keep one mount and follow another. Named for the keyword
stat(),walk()andcopy()already use for the same idea, rather
than a second vocabulary. -
Path.is_junction(),Path.is_mount()andPath.is_dir_binding():
a directory that is another tree's second NAME -- a Windows junction, or
a mount point such as a Linux bind mount -- is now a first-class concept
rather than a private hook used by one call site. Neither is a symlink
(is_symlink()is False, a non-following stat reports a plain
directory), which is precisely why a symlink check cannot protect a walk
from one.is_junction()matches pathlib 3.12's; both are False by
default and answered for real byLocalPath/FileUri. -
Path.glob(on_error=)/rglob(on_error=): a hook called as
on_error(error)when a directory cannot be listed, the same contract as
walk()andos.walk. Raising from it makes an unreadable directory
fatal; returning treats it as empty. Without the hook the listing is
skipped silently, as pathlib does and as before -- which left a caller
unable to tell an unreadable layer from an absent one.error.filename
names the directory even when the backend left it unset. -
Path.glob(bound_loops=)/rglob(bound_loops=): withTrue, a
directory is descended at most once per**, keyed on
(st_dev, st_ino)and seeded with the starting directory. This bounds a
Windows junction loop -- a junction reportsis_symlink() == False, so
recurse_symlinks=Falsecannot see it, and one file in a looping tree
matched 64 times (pathlib walks it the same way). DefaultFalsekeeps
pathlib's behaviour; a backend whose stat carries no identity is walked
unbounded. Both reported by yaconfiglib against 0.9.6.
Fixed
- An SFTP listing is untrusted input. The server chooses the names, and
they were turned into child paths unchecked, so a crafted../victim.txt
maderm(recursive=True)delete outside the tree it was given and a
recursive copy read from outside it (measured on both backends). Names
that are not a single component inside the directory are now skipped, as
dav:andhttp:already did. The asyncssh fan-out walkers, which bypass
_scandir(), filter for themselves. unpack_archive()split a backslash on every platform, so a POSIX
member whose name contains one was extracted as two path components and
one carrying..was silently dropped -- both legal single filenames
there. A backslash now separates only for a Windows destination, which is
the rule the function already documented.has_glob_pattern()was True for every Windows extended-length path.
The?in a\\?\C:\...anchor (and the\\?\UNC\...form) is a prefix,
not a wildcard, so a caller using it to tell a pattern from a plain path
got the wrong answer for all of them. The anchor is no longer scanned.
Reported by yaconfiglib against 0.9.6.- Writing over an archive member spelled
./f.txtappended a second
entry instead of rewriting it, andunlink()then deleted that second
entry and reported success while the original content came back. 0.9.5
handed the normalized name to a backend keyed on the raw one; writes now
address the entry the archive really holds. - Renaming a directory in such an archive silently did nothing, or
split it in two when its members were spelled inconsistently, while
rename()returned the new path as though it had worked. The backend now
receives the exact raw-name mapping instead of a normalized prefix. - Every archive operation rebuilt the member index, so a listing, stat,
read or write on a 20k-member archive scanned all 20k names: measured
10x-224x slower than 0.9.4. The index is cached per open handle and
dropped whenever the handle is, which every mutation and every external
change already go through. glob(None)ignorednative=, never auto-detected**, and answered
differently fromrglob(None)for the same path, because that branch
bypassed the pattern parser:LocalPath("/x/**/*.py").glob(None)returned
a shallow subset with no error.native=Falsealso left the trailing-**
rule following the interpreter, so it did not deliver the one-answer
promise it documents; it now pins that rule too./mangled adata:payload (data:,a/../bjoined todata:b/x,
not a data URI at all), dropped a scheme's own child handling (gitlab:'s
reserved-), still parsed abytesname as URI syntax, and lost
per-instance scheme state such asSftpPath'sssh_config. Joins now
walk segments through the same builder a listing uses.- A relative
strdestination whose first segment merely contained a
colon (notes:draft,Fedora-42:latest.tar) was read as a URI scheme
bycopy()/move(); the scheme must now be one a class registers, as
theuripathCLI already required. A Windows drive path (C:/Temp/x)
restarts the join for afile:path, asPureWindowsPathdoes. rename()andcopy()/move()resolved a relativestrdifferently
(rename("../b")sent a literalsub/../b, a different key on an object
store); both now resolve it the same way. A same-endpoint URI destination
reuses the configured connection instead of opening a second, bare one.
Changed
rm(recursive=True)no longer descends into a mount point. It
already removed a Windows junction as a binding rather than walking into
it; a POSIX bind mount is the same thing and was walked, so deleting a
tree containing one deleted the mounted filesystem's contents. It is now
rmdir()'d like a junction, which fails loudly on a live mount instead.UriPath / "name"andjoinpath()read astras a decoded path,
not as URI syntax.base / "cache?v=2"is now the filecache?v=2
instead ofbase/cachewith a query;"note#2.txt","a%20b.txt"and
"C:/Temp"join verbatim too. This is whatiterdir()always did, so
listing a directory and naming the same child by hand finally agree. Pass
aUri/UriPathargument for a scheme-aware join
(base / UriPath("s3://bucket/key")); that stays the only form that can
cross endpoints, and it still drops a credential-bearing backend on the
way. Dot segments are removed from the result as before.copy()/move()accept a plain-pathstrdestination. A string with
a scheme is a URI, as before, socopy("s3://bucket/key")keeps working;
one without is a decoded path on the same endpoint (absolute replaces the
path, relative is a sibling, asrename()resolves it) and reuses this
path's source and backend.move("b.txt")previously built a sourceless
path and failed on its firstexists()call, and a same-host URI
destination opened a second connection. A one-letter scheme is treated as
a Windows drive, soC:/Temp/xis a path.
Full Changelog: v0.9.6...v0.9.7