Repository navigation
v0.9.4
Fixed
-
Path.copy()destroyed or created the target when the source could not
be read. It unlinked an existing target (withoverwrite=True) and
opened the target for writing before opening the source, so a missing
file, a directory withoutrecursive=True, or a source HTTP 404 left the
target empty, or left a new 0-byte file that made a retry fail with
FileExistsError. The source is now opened first, and a copy that fails
mid-stream removes its partial target. -
Copying or moving a file onto itself deleted it.
f.copy(f, overwrite=True)(or onto a case-insensitive alias such asF.TXTon
Windows/macOS) emptied the file; a case-only rename with
move(overwrite=True)deleted it.copy()now raises
OSError(EINVAL, "Source and target are the same file");move()renames
in place. -
move(overwrite=True)removed the target before checking the source.
A missing source, or a file moved onto a directory, deleted the target
(including a whole tree) and only then raised. It now raises
FileNotFoundError/IsADirectoryErrorfirst and leaves the target
alone. A local file target is replaced atomically withos.replace(), so a
locked source on Windows no longer costs the target. -
rm(recursive=True)deleted files outside the tree through Windows
junctions andfile:directory symlinks. A junction reads as a
directory to a non-following stat, andUriPath's default_scandir()
used a following stat, so both were descended into and their targets'
contents deleted. Both are now removed as links.FileUrilistings also
reuseLocalPath's scandir metadata (one call per directory). -
PathSyncer.sync()could delete or write outside its target.- A root source that does not exist now raises
FileNotFoundError.
Before, withremove_missing=Trueit deleted the entire target (a typo,
an unmounted share, a 404); without it, it reported success. Callers
that relied on syncing an absent source as a no-op must now catch the
error or passignore_error. - Overlapping source and target (one inside the other, same
implementation and backend) now raiseValueError. Before, the source
could be deleted, or copies nested untilRecursionError. - A child name that would leave the target (
.., a name the parent-name
fallback turned into.., or\/:on a Windows target) now raises
ValueErrorthroughignore_error. Before, such entries from an S3,
SFTP or archive listing were written, or removed, outside the target. - Entries listed with an unknown stat (GitLab blobs, FTP without MLSD) are
now re-stat'd. Before, withfollow_symlinks=Falsenothing was copied
andremove_missing=Truedeleted the existing mirror. - A symlink inside the target is replaced by the real file or directory.
Before, sync listed, wrote and deleted through it, into whatever it
pointed at.
- A root source that does not exist now raises
-
http:/dav:listings yielded.and..as children. wsgidav's
parent row (<a href="..">) became a file named.., and unlinking it
deleted the parent collection; a./entry madewalk()loop forever; a
PROPFIND href%2E%2E/let a recursive copy write outside its destination.
Such names are no longer listed. -
DavPath.unlink()andHttpPath.unlink()deleted whole collections.
They sent a bareDELETE, which WebDAV applies recursively;unlink()on
a directory, andsymlink_to(force=True)over one, removed the tree. Both
now raiseIsADirectoryErrorfor a directory (HttpPathrelies on its
HEAD-based directory check).rm(recursive=True)still deletes trees. -
DavPathread an HTTP error page as file content.open("rb")/
read_bytes()/copy()on a missing or forbidden file returned the
server's 404/401/500 body. They now raiseFileNotFoundError/
PermissionError/OSError. -
Reading a local
zip:archive opened it for writing. A read-only zip
was unreadable (exists()returnedFalse),exists()on a missing
archive created it, and probing a file that is not a zip appended 22 bytes
to it. Reads now open the archive read-only; the first write into a
missing archive creates it. Probing a non-zip file now raises
zipfile.BadZipFile. -
Zip
unlink()/rename()/overwrite reset every other member. The
rewrite gave all members the current time, DEFLATE compression and mode
0600, and dropped the archive comment and any leading bytes (a zipapp
shebang); it also replaced a symlinked archive with a regular file. Member
metadata, the comment, the prefix bytes and the archive's file mode are
now kept, and a symlinked archive stays a symlink. -
Zip
rename()onto an existing member created a duplicate name, and a
later rewrite kept the old content. It now replaces the target (POSIX
semantics; seedocs/divergences.md). -
Archive listings exposed traversal member names. Members named with
.., an absolute path,\-separated traversal or a drive prefix are no
longer listed, soiterdir()/walk()/copy(recursive=True)cannot write
outside a destination through them. -
utils.unpack_archive()let crafted members escapedeston Windows
(D:evil.txt, and the same-driveC:../C:../x). Such members, and any
member with a..part (previously extracted with the..dropped), are
now skipped. -
rename()/move()renamed onto the wrong host, bucket or archive.
Every scheme renamed through its own connection or bucket with only the
target's path:SftpPath/FtpPathmoves to another server renamed on the
source server,S3Path/GsPathmoves to another bucket landed in the
source bucket (overwriting an existing object of that name there), a zip
member moved to a local path was renamed inside the archive, and
LocalPath.move()onto a remote path renamed a local file.rename()now
raisesNotImplementedErrorfor a target on another endpoint, archive or
Azure container, andmove()copies and deletes instead.move()also
falls back to copy + delete on a cross-device rename (EXDEV), and
SftpPath.copy(recursive=True)uses its concurrent fan-out only for a
target on the same host. -
Session credentials and tokens followed a join to another host.
base / "http://other/x",UriPath(base, url)andbase.with_source(...)
reusedbase's backend, so anHttpPath.with_session(auth=...)session or
agithub://TOKEN@...token was sent to the other host. A backend is now
reused only for the same scheme, userinfo, host and port. -
AzPath.rename()with astrtarget always raisedTypeError, and a
pending copy crashed withKeyErrorafter starting it.GsPath/AzPath/
S3Path.rename()onto the same key deleted the object. Both fixed. -
FileUri.rename("b.txt")resolved against the process cwd and returned
aLocalPath. It now renames within the same directory and returns a
FileUri. -
MemPath.copy("/b.txt")/move("/c.txt")wrote into a new, empty
in-memory filesystem, andmove()then deleted the source. Astr
destination now stays on the source's backend. -
SftpPath.copy()raisedModuleNotFoundErrorwithout asyncssh
(paramiko-onlysftpextra), including every single-file download and
PathSyncerwith an SFTP source. -
SFTP connections leaked or went stale. A first-call asyncssh
rm(recursive=True)hung for 60 s and deleted nothing; dropped paramiko
connections and closed asyncssh SFTP channels were never replaced, so every
later call failed andexists()returnedFalse; evicted connections,
failed logins and failed SFTP starts leaked sockets and threads; concurrent
first calls opened duplicate connections; a recursive copy held two remote
handles open per file in the tree;SftpPath(url, ssh_config=...)ignored
ssh_config; the paramiko backend ignored ssh_configInclude. -
UriPath("ftps://...")returned a stubUriPathin a fresh process
instead ofFtpPath. -
URL credentials leaked into HTTP errors and redirects. They are now sent
as Basicauth=instead of inside the request URL, WebDAVMOVE
Destinationno longer carries them, and translated errors no longer chain
therequestsexception (__cause__isNone; the message carries the
HTTP status and reason). URL credentials now take priority over a matching
~/.netrcentry. -
github:/gitlab:tokens leaked throughstr()/repr()/errors, and
user:TOKEN@hostauthenticated with the username. The token is now read
from the password slot when present, and these schemes redact the whole
userinfo. -
glob()/rglob()crashed, hung or returned wrong results.
glob("**")andglob("dir/**")raisedNotADirectoryErroron any tree
containing a file;**followed directory symlinks, so a symlink loop
produced duplicates effectively forever; globbing under a missing directory
or a file raised instead of yielding nothing; repeated**returned
duplicates; a trailing/matched files onMemPathand URI paths;?
never matched on aUriPath(read as a query);glob.full_match()slowed
down exponentially with repeated**. A trailing**now follows the
running Python (files too on 3.13+). -
match()onMemPath,Uriand everyUriPathdid not follow
pathlib. It anchored at the start and let*cross/, and a URI's
host:prefix defeated absolute patterns. It is now pathlib's
right-anchored per-segment match; an empty pattern raisesValueError.
LocalPathacceptsmatch(case_sensitive=)on 3.9-3.11, and its
full_match()handles rooted, drive and backslash patterns before 3.13. -
parents/parentof an absoluteMemPathorUrilost the root.
MemPath('/a/b').parentsis now['/a', '/'];Uri('http://h/a').parentis
http://h/; a top-levelFileUri's parent is/(or the drive root
C:/on Windows) instead of resolving to the current directory.
relative_to()/is_relative_to()treats3://bucket/http://has the
root, sorelative_to(p.parent)andwalk_up=Truework. -
with_name()/with_stem()/with_suffix()accepted'',.and
separators onMemPathandUri, splicingx/yor../../etcinto a
path. They now raiseValueErrorlike pathlib. -
MemPathdid not normalize likePurePosixPath.MemPath('/') / 'a'
was//aand unequal toMemPath('/a'); a trailing/changed equality;
an absolute join did not reset. All now matchPurePosixPath. -
Path.touch()made new files world-writable and could truncate existing
ones. It chmod'ed every new file to 0o666 ignoring the umask (file:,
SFTP, FTP) and treated anystat()error as "missing".modenow defaults
toNone(chmod only when passed), an existing file is never truncated, and
FileUri.touch()uses pathlib'stouch(). -
copy()made local copies read-only.preserve_metadata=Trueapplied
the placeholder 0o444/0o555 mode thatMemPath, HTTP, WebDAV, object stores
and archives report, so a copied file could not be overwritten or re-synced.
Placeholder modes are no longer applied (FileStat.mode_known). -
utils.parsedate()read GMT dates as local time, so every HTTP/WebDAV
st_mtimewas off by the host's UTC offset, and it raisedOverflowError
on Windows east of UTC. It now returns UTC epoch seconds and passes numbers
through. -
s3://b/dir/(trailing slash) was treated as the marker object: not a
directory, andrm(recursive=True)removed only the marker. S3, GCS and
Azure keys now drop one trailing/. Azure keys keep interior empty
segments (a//b) as written. -
GsBackendrewrote the process-wideSTORAGE_EMULATOR_HOSTand dropped
otherclient_options. Keyword arguments now go tostorage.Client
unchanged; for an emulator also passuse_auth_w_custom_endpoint=False. -
URI queries were corrupted on the wire. They were percent-decoded at
parse time and re-encoded with&,=and+treated as safe, so a signed
URL'ssig=ab%2Bcd%3D%3Dreached the server asab+cd==and an escaped&
split a value;with_query(dict)double-encoded.Uri.queryis now kept as
received and sent unchanged (see Changed). -
A remote path joined with a relative
pathlib.Pathbecame a local
file.UriPath("sftp://h/srv/") / pathlib.Path("etc/x")produced
file:/srv/etc/x, so reads and writes hit the local disk. A relative path
now joins like aPurePathand stays on the remote; only an absolute local
path becomesfile:.UriPath.joinpath()picks the class from the scheme. -
URIs with non-UTF-8 percent-escapes (
caf%E9.html) raised
UnicodeDecodeError; they now construct and round-trip.data:payloads
are no longer dot-normalized or decoded twice, and binary payloads work. -
Windows
file:URIs:file://localhost/C:/...could not be printed,
hashed or compared; afile://<host>/sharewhose host is this machine
mapped to the current drive instead of a UNC path. -
Scheme registry: a
UriPathsubclass defined after the first dispatch
was never found, and every unknown scheme rescanned entry points. -
open()modes:rt/wtfailed on every non-local backend and invalid
modes raisedNotImplementedError; modes are now validated like the
built-inopen()(ValueError).open("r+")on buffered backends (FTP,
S3, GCS, Azure, local zip members) returned a writable buffer whose writes
were discarded; writes are now uploaded on close (orNotImplementedError
where impossible). -
copy():copy(recursive=True)into its own subtree recursed without
limit;copy(follow_symlinks=False)copied the link target's content with
the link's 0o777 mode (it now recreates the link, as pathlib 3.14 does);
overwrite=Falsewas decided byexists(), which reads a transient 503 as
"missing". Downstream classes mixing a concrete stdlib path withPathnow
also get pathlib_next'sstat/chmod/glob/walk/_scandir. -
MemPath:iterdir()on a missing path raisedNotADirectoryError;
files always reportedst_mtime=0, soPathSyncerskipped same-size edits. -
Archives (
zip:/tar:/archive:): children of the archive root were
named/nameand matched no member, soiterdir/glob/recursivecopy
from the root failed; tarballs with./members (tar -C dir .,
shutil.make_archive) were unreadable; adding a zip member rewrote the
central directory in place (a crash corrupted the archive); a cached handle
ignored changes by other writers and kept the file locked on Windows;
archive URIs did not round-trip names with#,?or%; concurrent tar
reads returned wrong bytes; stored member modes were ignored;iterdir()on
a file returned[].utils.make_archive()now accepts anyPathsource,
writes the target only when complete and supports zip64;
utils.unpack_archive()accepts non-seekable streams and extracts tar links. -
HTTP/WebDAV: gzip-encoded responses were returned compressed (and
rewrite-mode append re-uploaded them); a redirect madestat()report a
directory; listing hints fabricated sizes used as the patch-append offset;
mid-body read failures raised urllib3 exceptions;DavPathlisted a
directory with a space in its name as its own child, treated a 207
Multi-Status with failed members as success, re-sent a failed PUT at garbage
collection and raised rawrequests.HTTPErrors;with_session(headers=...)
was replaced by internal headers. -
GitHub/GitLab: GitLab listings stopped after 100 entries (and
stat()
called later subdirectories missing); GitHub listings stopped at 1,000;
self-hosted API roots dropped the port; GitLab rootstat()answered from
the URI shape without asking the server. -
uripathCLI:synccompared sizes only, so same-size edits were never
copied;--dry-runprinted nothing;read/cp -buffered whole objects. -
FTP: every separately built path opened its own connection; an error
mid-transfer desynchronized the cached connection for good; a write whose
connection timed out while idle lost its data; without MLSD, directories
stat'ed as missing; MLSD mtimes were read as local time; permission errors
surfaced asFileNotFoundErrorand rawftpliberrors escaped. -
S3/GCS/Azure: botocore
ClientErrorescapedexists()/walk()and a
403 read as "missing"; GCS/Azure turned every exception (including a missing
SDK) into "does not exist", socopy(overwrite=False)could overwrite;
iterdir()on a missing path returned[]andrmdir()/unlink()accepted
wrong-type targets; a failed upload was retried at garbage collection over
newer data; prefix-directorymove()failed; S3 objects above 5 GiB could
not be written or renamed;open("x")was a check-then-put race;
AzPathwithoutbackend=ignored the URI's account; Azure recursiverm()
stopped at the first failing blob in a batch. -
PathSyncer: an interrupted copy lost the previous version (it now
writes a temporary sibling and renames); preserve mode deleted the target
before discovering symlinks were unsupported; dry runs crashed on new
subdirectories;RemovedMissingevents carried the parent directory;
ignore_errorwas called once per ancestor with the wrong paths; two
unknown (0) mtimes counted as "in sync"; FIFOs and devices replaced the
target with an empty directory. -
SFTP:
rename()onto an existing file failed with a bare
OSError("Failure");unlink(missing_ok=True)skipped dangling symlinks;
a relativereadlink()result could not be printed; asyncssh file handles
made one round trip per byte inreadline()and an unclosed handle hung
interpreter exit for 60 s; the asyncssh recursive copy called a bool
ignore_errorand ignored the own-subtree and symlink rules; paramiko's
native checksum probed an extension OpenSSH does not implement, paying extra
round trips per file. -
rename()returnedNoneondav:,s3:,gs:,az:,ftp:and
sftp:; it now returns the new path, as pathlib does. -
Wrong exception types on
sftp:,dav:,gitlab:andMemPath.
Listing orrmdir()of a file raisesNotADirectoryError,rmdir()of a
non-empty directoryOSError(ENOTEMPTY)(MemPathraised
FileExistsError), opening or unlinking a directoryIsADirectoryError;
dav:reading a collection raised nothing and returned its HTML index, and
rmdir()of a missing path raisedNotADirectoryError. paramiko
open("x")returned a file that could not be written. -
Path.rm(recursive=True, ignore_error=callable)offered a declined
error to the callable again from every enclosing directory. -
uripathcrashed at import without theuriextra, even for local
files. Local paths and-now work; a URI argument reports the extra to
install. Without the extra it also read every colon name as a URI
(uripath read notes:draftasked forpathlib-next[uri]instead of
reading the file); the schemes this package registers are now read from
its entry points, so a colon name behaves the same in either install. -
match()disagreed with pathlib on Python 3.12 at the root. 3.12
matches the whole path as one string with its separators swapped for
newlines, so"/"is a single newline that"**"matches from either
side ("/".match("**"),match("/**")andmatch("**/**")are all
Truethere) while"*"never does, and a bracket expression such as
"[!a]"consumes the separator itself.MemPath/UriansweredFalse
throughout; 3.12 now runs a port of that algorithm instead of the
part-by-part comparison every other version uses. -
import pathlib_nextimportednetimps(a host-name query at import,
slow on Windows), and the firstfile:/data:path importedrequests
andbotocore; both now load on first use.GsPath/AzPathare exported
frompathlib_next.uri.schemes. -
*.local.*files shipped in the sdist and wheel. -
The SFTP/FTP/WebDAV examples built URIs from unencoded credentials (a
password containing/,#,?or@changed the host) and printed the
password. -
benchmarks/bench.pycrashed at import on Python 3.9. -
Docs: the CI benchmark table had its Ubuntu, Windows and macOS columns
rotated; the paramiko single-file write/copy slowdown was on Ubuntu. -
Core, MemPath and URI edge cases.
open()leaked the backend handle
when the text wrapper failed; synthesized errors fromrm()/touch()and
MemPathlackederrno/filename;copy(progress=)never reported a
zero-byte file;samefile(str)lost the backend or host;MemPathhandles
appended at the seek position, hid unflushed writes and accepted writes on
read handles, and exclusive create/mkdircould both succeed under
concurrency;FileStat.from_stat()keptNonefields; checksums failed on
FIPS hosts (usedforsecurity=False);is_dir()/is_file()rejected
follow_symlinks=;"prefix" / pathwas unsupported; suffix/stem ignored
3.14's rules; lazy URI parsing could expose unset components to another
thread;Uri.__eq__raised against a relative local path; a//path with
no authority could not be rendered; non-ASCII hosts were sent
percent-encoded;Uri("/a").is_absolute()wasFalse;data:accepted
r+and discarded writes. -
Scheme and CLI edge cases. FTP listed MLSD
cdir/pdirentries named
like children; archives nested in archives could not be addressed;
GitLabiterdir()on a file yielded nothing; GitHub/GitLab 429 and
secondary rate limits were not recognised;git://<ip>raised
AttributeError; a customBaseRepoBackendwithout a cache crashed on
GitLab; git-hostingopen("r+")returned a writable buffer;uripath cp -
overwrote an existing target without--overwrite, a local name with a
colon was treated as a URI, and a closed pipe or Ctrl-C printed errors;
HttpPath.iterdir()on a file downloaded it; HTTP 409 on PUT and 410 were
mis-mapped; WebDAV read only the first<propstat>; HTTP listings behind a
prefix were scoped by the page title; S3/GCS listings disagreed with
stat()when a key was both an object and a prefix; GCS/Azure roots always
reported existing; asyncssh SFTP errors had noerrno/filename. -
PathSynceredge cases. Tolerated errors left no trace; a directory
that became a file or symlink in the source deleted the target directory
even withremove_missing=False;SyncStartpassed raw paths to the hook
and dry runs reporteddry_run=Falsefor traversal events; preserved
directory symlinks were created as file links on Windows; an identical
symlink was recreated on every run;PathAndStat(path)described a symlink
itself instead of following it.
Changed
PathSyncerkeeps non-empty target directories on a type change unless
remove_missing=True(IsADirectoryErrorthroughignore_error,
SyncEvent.TypeMismatch). Every tolerated error is now logged at WARNING
onpathlib_next.syncand reported to the hook asSyncEvent.Error.
PathAndStatfollows symlinks by default.PathSyncer.hook()gains a
keyword-onlyalways_run.- URI comparisons and rendering:
Uri == <non-URI Pathname>(e.g. a
LocalPath) is nowFalse(aUristill equals anotherUrior a URI
string); non-ASCII hosts are rendered in IDNA form; an explicit
schemesmap=is authoritative (an unknown scheme gives a plainUriPath);
with_source()with a scheme-less source returns a plainUriPath;/no
longer hides aTypeErrorraised inside a scheme class. data:URIs rejectr+, decode base64 only with;base64, and imply
text/plainfor a parameters-only header.uripathexits 141 on a closed stdout and 130 on Ctrl-C.- Package metadata uses PEP 639 (
License-Expression: MIT) instead of
theLicense ::classifier, and addsTyping :: Typed,
Development Status :: 4 - Betaand Python 3.9-3.14 classifiers. Building
from source needshatchling>=1.27. - The
azextra installsazure-identity, which anAzPathwithout
backend=needs for its default credential. pathlib_next.testingcontracts are stricter: 48PathContracttests
(was 20) covering pathlib error types, glob, walk, rename, open modes and
recursive copy. A backend that cannot meet a rule sets a capability
attribute toFalse(supports_listing,supports_empty_directories,
distinguishes_file_types,supports_rename,supports_append,
supports_exclusive_create,enforces_directory_hierarchy). The contract
root must be a fresh, function-scoped directory populated with
populate_fixture_tree().test_iterdir_lists_childrenno longer passes
wheniterdir()is unimplemented.Uri.queryis the percent-encoded query as received and is sent
unchanged;Query(...).decode()/to_dict()decode each name and value
once. Astrpassed towith_query()is taken as already encoded; a
mapping's keys now escape=. Code that read.queryexpecting decoded
text must decode it.- Archive paths raise pathlib's POSIX exception types (
iterdir()on a
file, reading orunlink()ing a directory,rmdir()on a file), and
mkdir()/new zip members need an existing parent. Ziprename()returns the
new path. Archiveas_uri()percent-encodes the member path. uripath synccompares file content by default;--size-onlyrestores
the old comparison.--dry-runprints planned changes and-v/--verbose
prints changes made.gitlab:reads a-segment at position 3 or later as GitLab's/-/
separator (gitlab://host/group/sub/project/-/path).SftpPath.rename()replaces an existing target where the server
supportsposix-rename@openssh.com, else raisesFileExistsError.DavPathmaps request errors to pathlib exceptions (PUT/MOVE into a
missing parent:FileNotFoundError; 423:PermissionError).Path.glob()/rglob()/LocalPath.glob()include hidden files and
directories by default, as pathlib does. Passinclude_hidden=Falsefor
the old results.glob("")now raisesValueErrorand an absolute pattern
raisesglob.NonRelativePatternError; before,""yielded the base and an
absolute pattern listed outside it.- SFTP host keys are verified by default on both backends. Before, any
server key was accepted (asyncssh even overrode ssh_config pinning), so a
man-in-the-middle received the URI password. Now an unknown or changed key
fails before credentials are sent. paramiko uses~/.ssh/known_hosts,
ssh_configUserKnownHostsFileandRejectPolicy. To keep the old
behaviour add the host toknown_hosts, or opt out explicitly:
SftpBackend(connect_opts, paramiko.AutoAddPolicy(), known_hosts=None)/
AsyncsshSftpBackend(connect_opts={"known_hosts": None}). ftps:verifies the server certificate and host name by default.
Before, any certificate was accepted and the password sent to it. For a
private CA passFtpBackend(ssl_context=ssl.create_default_context(cafile=...));
FtpBackend(verify=False)disables verification. Data connections now reuse
the TLS session (vsftpd, FileZilla Server).- Network operations have default timeouts. HTTP/WebDAV/github/gitlab:
(10, 60)s connect/read (with_session(..., timeout=...),
RepoBackend(timeout=...)); FTP: 30 s (FtpBackend(timeout=...)); paramiko
connect/banner/auth/channel-open: 30 s (SftpBackend(..., timeout=...)).
timeout=Nonerestores the unbounded wait. Before, a stalled server hung
the caller forever. - asyncssh timeouts apply to single requests only
(AsyncsshSftpBackend(timeout=...), default 60 s). Recursive
copy()/rm()andread_bytes()/write_bytes()no longer raise after
60 s while the work continued in the background. A timed-out request is
cancelled and raises the builtinTimeoutErroron every Python version (on
3.9/3.10 it wasconcurrent.futures.TimeoutError). - paramiko backend: ssh_config
ProxyJumpraisesNotImplementedError.
Before, it was ignored and the connection went direct. Use asyncssh, a
ProxyCommand, orconnect_opts["sock"]. - asyncssh
rm()/copy()error callbacks run in a worker thread, so they
may call ordinary path methods; a sync SFTP call made on the bridge-loop
thread raisesRuntimeErrorinstead of hanging.
Added
SyncEvent.ErrorandSyncEvent.TypeMismatchreporting;str / path
(__rtruediv__) onPathnameandUri.pathlib_next.testing.populate_fixture_tree(root)andFIXTURE_TREE.benchmarks/bench.py --save(min/median/max ms per call as JSON under
benchmarks/results/) and--samples.SyncEvent.CompareandSyncEvent.Skipped;uripath sync --size-onlyand
-v/--verbose.glob.parse_pattern(),glob.select(),glob.NonRelativePatternError;
recurse_symlinks=Falseonglob()/rglob();FileStat.mode_known.close()onSftpBackend/AsyncsshSftpBackend;FtpBackend(timeout=, ssl_context=, verify=);RepoBackend(timeout=);utils.LRU(on_evict=...)
andLRU.discard().utils.is_safe_child_name(name, *, windows=False)and
utils.is_windows_flavoured(path): check that an untrusted name stays a
single component inside its parent before joining it onto a destination.
Full Changelog: v0.9.3...v0.9.4