Repository navigation
v0.9.13
Added
copy_into(),move_into()andreplace()on everyPath. pathlib 3.14
has them and the generic classes did not (MemPath,UriPath,DavPath,
SftpPath, ... raisedAttributeError;LocalPathhadcopy_into()and
move_into()on 3.14 only).copy_into(target_dir, ...)copies under the
same name withcopy()'s keywords and defaults,move_into(target_dir, *, overwrite=False)moves withmove()'s, and both return the new path.
replace(target)ismove(target, overwrite=True)and returnstarget; a
directory is replaced only when empty (OSError(ENOTEMPTY)otherwise, and
nothing is removed). Astrdirectory or target stays on the same backend.
infois not added.Pathname.is_absolute()has a default: the path has a root. It raised
NotImplementedErrorforMemPathand everyPathnamesubclass that did not
override it.- Generic paths can be ordered.
sorted(),<,<=,>and>=raised
TypeErrorforMemPath,Uri,UriPathand every customPath.
Pathnamenow orders two paths of the same exact type by the key its==
uses (the segments; the URI text forUri), and returnsNotImplemented
for anything else, aspathlibdoes across flavours. A subclass with its
own__eq__overrides_order_key()to match it.LocalPathkeeps
pathlib's ordering. pathlib_next.__version__and__all__on the core modules. The root has
__version__, the installed distribution's version ("0+unknown"for a
source tree that is not installed). The root,path,fspath,protocols
(withfs,ioandchecksum),utils,utils.statandutils.glob
declare__all__with the namessrc/pathlib_next/AGENTS.mddocuments for
them. Nothing documented moved.InsecureTransportWarning. ARepoBackend(github:,gitlab:,git:)
that is about to send anAuthorizationheader (its token, or one passed as
headers=) to a plainhttp://API root on a host other than loopback now
warns with thisUserWarningsubclass (importable from
pathlib_next.uri.schemes._gitrepo,.githuband.gitlab); the request is
still sent. Use anhttps://api_base, or filter the warning for a
trusted network.SftpAuthenticationErrorandSftpHostKeyError. Importable from
pathlib_next.uri.schemes.sftp. A refused login is the first (a
PermissionError,EACCES) and a host key that is unknown, changed or
refused the second (aConnectionError,ECONNABORTED), on both SFTP
backends. CatchOSError, or these two to tell a login from a host key.ArchiveUri.refresh(). A remote archive (zip:http://...!/m) is read once
and kept while any path to it lives, so a change at its source was never seen
by a program that held a path.path.refresh()forgets what is held of the
archive, for every path to it, and the next use reads it again. A local
archive is checked against its file on every use and needs none.GsBackend(timeout=, retry=). Both go to every call aGsPathmakes
(timeoutin seconds or a(connect, read)pair,retrya
google.api_core.retry.RetryorNonefor none). Without them the SDK's
defaults apply, and an unreachable endpoint kept every call waiting for about
two minutes. A customBaseGsBackendadds options throughcall_options().schemes.az.COPY_POLL_TIMEOUT. How longAzPath.rename()waits for the
server-side copy (300 seconds); set it before the call to change it.pathlib_next.testing: five capability switches and eight tests. For a
contract class that subclassesPathContract, the new tests are:
a payload of about 290 KiB holding every byte value and runs of CR/LF, written
whole and in pieces and read back whole and in parts; names holding a space,
#,%,+,&,=and mixed case, stored, listed, renamed and removed as
written;stat()of a longer file and of a
directory, and a numericst_mtime;_scandir()agreeing withstat()for every child; nothing
existing below a file; partialread(n); andrename()onto a non-empty
directory raising. The switches, each documented in the module and in the
guide:supports_mkdir,supports_deleteandsupports_move(default
True: a backend with no directories, delete or move, such as TFTP, sets them
Falseinstead of marking tests as expected failures),supports_unusual_names
(defaultTrue), andsupports_question_mark_names(defaultFalse: a
Windows file system cannot store a name holding?). A backend that kept
passing the old suite and now fails a new test was altering bytes, sizes or
names: fix it, or set the switch that names the gap. With
supports_delete = False,copy(overwrite=True)is no longer exercised;
enforces_directory_hierarchy = Falsealso skips the rename-onto-a-directory
test.
Changed
from pathlib_next import *publishes only the documented names. It also
broughtP,PNand the__future__nameannotations, and
from pathlib_next.protocols import *andfrom pathlib_next.utils import *
brought the modules they import.P,PN,K,Vand the rest stay
importable from the module that defines them; a script that relied on
from pathlib_next import *for one of them imports it by name.- The constructors of
Path,LocalPath,MemPathandFileStatpass mypy
and pyright at their defaults. Both readPath("x"),LocalPath("x"),
MemPath("/a")andFileStat(...)as constructing an abstract class (the
protocol stubs had empty bodies,FileStat's fields were not declared, and
Pathname's abstract members counted against the barePath, which builds a
LocalPath). The stubs now raiseNotImplementedErrorthrough a call,
FileStatdeclares its fields,Path.__new__is annotated, and a checker
treatsPathname's abstract members as ordinary methods. Nothing changes at
run time: ABC enforcement still refuses a subclass that omits one. A consumer
can drop a# type: ignore[abstract]on those four constructions.
FileStatLike.st_mtimeis annotatedfloat, which is what every backend
reports. - On Python 3.14
LocalPath.copy_into()andmove_into()are this
library's, not stdlib's. Stdlib's overwrote an existing file, copied
directories without being asked andLocalPathanswered differently from
every other backend. They now behave asMemPath's do: an existing target
raisesFileExistsErrorunlessoverwrite=True, a directory needs
recursive=True, and the new path is returned. Passoverwrite=True, recursive=Truefor the stdlib behaviour.LocalPath.replace()stays
stdlib'sos.replace()on every version. with_stem("")raises on 3.13 and later when the name has a suffix, as
pathlibdoes there (MemPath("/a/c.txt").with_stem("")was/a/.txt).
Before 3.13 it is unchanged.walk()lists a Windows junction infilenamesand does not enter it
unlessfollow_symlinks=True, as it already did for a symlink to a directory
and aspathlib.Path.walk()does on 3.14. It listed the junction in
dirnamesand descended: a junction to its own parent was walked 64 levels
deep, until the path was too long. A caller that wants the target's files
passesfollow_symlinks=True. The header anddocs/divergences.mdno longer
say thatLocalPathprotects a following walk from a loop; neither does
pathlib.utils.as_mode()andutils.as_owner()refuse what cannot be a mode or an
owner.as_mode(-1)returned-1,as_mode("07777777777777")a number
wider than any mode, andas_owner(True, False)was(1, 0)on 3.14 and
(True, False)on 3.9. Aboolis nowTypeError, a mode outside
0..0o177777(ast_modewith its type bits still passes, asos.chmod
accepts it;stat.S_IMODE()drops them) and an id outside0..2**32-1
(-1stays "unchanged")ValueError.UriPath(url)type-checks under pyright.UriPath.__new__annotated
schemesmapasdictwith a default ofNone, which pyright rejects for
every construction (UriPath("sftp://h/x")); it is now an optional mapping of
scheme to class.ReadPathContracthas a pickle test and asupports_pickleswitch. A
contract subclass checks thatpickle.loads(pickle.dumps(path))is equal to
the path, hashes alike, and for a URI path is placed on the same filesystem.
An implementation whose paths cannot pickle setssupports_pickle = False
on its test class and the test skips.Uri(0),Uri(5)and other falsy non-text arguments raiseTypeError.
OnlyNoneand empty text are the empty URI;Uri(0)orUri([])used to
be silently the empty URI, andUri(5)said it gotNoneType. The message
now names the type that was passed. PassNoneor""for an empty part.- An
http:directory index larger than 8 MiB raises.iterdir(),
walk()andglob()read the whole index and parse it at about a second per
megabyte with no bound, so a server decided how long a listing took. A body
overpathlib_next.uri.schemes.http.MAX_LISTING_BYTES(8 MiB after decoding)
is nowOSError(EFBIG), checked againstContent-Lengthbefore the body
is read. Assign a larger number to that module constant to list a bigger
index. - Listing an
http:subdirectory andstat()of a redirecting URL send fewer
requests. A directory without a trailing slash is now asked for with it
first (oneGETinstead of aGETand the redirect it earned, per
directory of awalk()), falling back to the path as given on a 404, and
stat()follows theLocationof the first answer instead of repeating the
request that was redirected. - The header and the schemes guide say that only
Authorizationis dropped
from a redirectedGET/HEAD.with_session(headers={"X-Api-Key": ...})
has always been sent to the new host whenrequestsfollows a redirect to
another one; nothing changed in the code. Keep a secret inAuthorization
orauth=, or point the path at the final URL. stat()of agitlab:file asks withHEAD. It was aGETof the files
endpoint, whose reply carries the file base64-encoded, so everystat(),
exists()andis_file()of a large file (and every file met bywalk(),
glob()orcopy(), which get no size from the tree listing) downloaded
it. The size now comes from theX-Gitlab-Sizeheader of aHEAD; a reply
without it, or a server that refusesHEAD, is followed by theGETof the
metadata as before.github:still reads the contents object.- Paths derived from one another on one endpoint share the backend the first
of them builds.root / nameon a fresh root, orp.parentbesidep,
built a backend (anhttp:session, aRepoBackendwith its own
default-branch cache, an SFTP connection) per path on first use: 20 children
of one root opened 20 connections, one TLS handshake each, where they open
one afterroothas done I/O itself, andgitlab:looked the default branch
up once per child. They now share one slot for the backend they derive for
themselves; deriving a path still builds nothing and imports no I/O library,
a thread race builds one, and a supplied backend (backend=,
with_backend(),with_session()) is carried as before and never replaced.
Paths constructed separately, on another endpoint, or with no source (the
result ofrelative_to()) do not share; twoUriPath(url)calls still make
two connections, so build the root once and derive from it. uripath.main()returns the exit status of a wrong invocation and of
--help. It raisedSystemExit(2)andSystemExit(0), although it is
documented to return anint. The console script's exit status is the same;
a program that calledmain()and caughtSystemExitreads the returned
status instead.- The SSH library's message for a failed SFTP handshake is logged at DEBUG.
A handshake or session failure that is neither a refused login nor a host key
is aConnectionAbortedErrorwhose text names only the library's exception
class (SFTP connection failed (KeyExchangeFailed)), so the reason was lost.
The library's message is now logged on the loggerpathlib_next.sftpat
DEBUG, on both backends; the exception is unchanged. The record can hold what
the library put in its message, including a credential, so enable that logger
for troubleshooting only.
Fixed
copy()makes twostat()calls per file, not three to five. The source's
type, mode and size come from onestat()and the target's existence and
type from another, and two stats that carryst_dev/st_inoare compared
rather than askingsamefile()for stats that aFileStatbackend cannot
answer. On a remote scheme each is a request. Everythingcopy()refused it
still refuses: the same file under two spellings, an existing target without
overwrite=True, a directory in the way. A move by copy and delete saves one
more.LRU(maxsize=0)stores nothing. It stored the new value, evicted it at
once and handed it toon_evict(a connection cache closed the connection it
was about to return).maxsize=Nonewas aTypeError; it is now unbounded,
as forfunctools.lru_cache.FileStat(st_mode=0o644)is a file, andFileStat(st_mode=0o755, is_dir=True)a directory.is_dirwas consulted only whenst_modewas
empty, so a bare permission mode was neither. The type bits are now ORed in; a
mode that carries a type keeps it.setmode()read a block device or a
socket as a directory.full_match()on a generic path treats the root as a component of its own.
MemPath("/a/b.txt").full_match("*/*/*.txt")wasTrue(the first*
matched the empty root),MemPath("/").full_match("/**")wasFalse,
MemPath("a/b").full_match("a/./b")wasFalseandMemPath("").full_match("")
wasFalse;pathlib3.13 answersFalse,True,True,True. A rooted
path now needs a rooted pattern or a leading**that reaches past the root,
a lone*does not match it, and.and empty pattern components are
ignored.Urishares the fix. A bracket expression still never matches a
separator (seedocs/divergences.md). A path oros.PathLikeis accepted as
the pattern.- The 3.12
match()pass swaps newlines and separators in both directions.
A newline inside a name was read as a separator, so on Python 3.12
MemPath("a\nb").match("a/b")wasTrue. Other interpreters are unchanged. LocalPath.symlink_to("./t/")stores./t/on POSIX, aspathlibdoes; it
storedt(andt//xast/x) because the text was parsed as a path first.
Windows still rewrites a relative target into its own separators, without
which the link would not resolve. A genericPathbuilds astrtarget with
with_segments(), so a subclass with per-instance state (aMemPath
backend) no longer receives a target bound to a fresh one.utils.parsedate()says what it accepts.bytes, adatetimeand a
tuple of fewer than six items raise, and "31 Feb" rolls over to March; the
docstring and the header now say so. Nothing else changed.LocalPath.copy()andmove()arePath.copy()andPath.move()
(LocalPath.copy is Path.copy): the two forwarding methods are gone, and with
them one call frame. A subclass that overrode the private
Path._is_junction_link()overridesis_dir_binding()instead; the alias is
gone.- The header no longer says
LocalPath.is_*()returnFalsefor any
OSErroron every Python version.exists()does;is_dir(),is_file(),
is_fifo(),is_socket(),is_block_device()andis_char_device()are
stdlib's and, before 3.13, raise for an error such asPermissionError. The
behaviour is unchanged. - Joining a
MemPathonto another keeps the left operand's tree.
root / MemPath("f.txt"),root.joinpath(MemPath("f.txt"))and
MemPath(root, MemPath("f.txt"))addressed the last argument's own empty
backend, so.exists()wasFalsefor a filerootheld, and
utils.glob.glob(MemPath("*.py"), root_dir=src)found nothing. The first
MemPathargument now names the backend; an explicitbackend=still wins.
Passbackend=to place a join on a different tree. MemPathapplies..to the tree./missing/../a.txtexisted,
/a.txt/../dwas a directory and/missing/../newwas created as/new,
because..was removed from the text first. As on a POSIX filesystem, the
directory a..leaves must exist (FileNotFoundError) and be a directory
(NotADirectoryError);..above the root stays at the root. A spelling
through a directory that exists names the same node as before.- Two
MemPathappend handles both land, and a write never shows a reader
an empty file. Each handle opened with"ab"published its own snapshot
of the whole file on close, so the second close overwrote the first's bytes
(b"02"for appends of1and2to0), and every publish emptied the
file before refilling it. A handle now adds only what it wrote, in one step
(b"012"), and a write replaces the content in one step. MemPath.rmdir()of the root raisesOSError(EBUSY), not
FileNotFoundError. The root exists, andFileNotFoundErrorread as
"already gone" to a caller that catches it;root.rm(recursive=True)emptied
the tree and then failed with it. The tree is still emptied; catchOSError
if you call it on a root.MemPath.segmentsis a tuple and an unknown keyword is aTypeError.
The list was the path's own:p.segments.append("c")changedstr(p)and
its hash.MemPath("/", bakend=b)silently built a fresh backend. Code that
mutatedsegmentsor passed a stray keyword was already wrong; read a copy
withlist(p.segments).- On Python 3.9 a copied or pickled
MemPathfile keeps itsst_mtime.
copy,deepcopyandpickleof aMemFile(and of aMemPathBackend)
gave every file the time of the copy on 3.9; 3.14 already kept it. A sync
quick check that compares(size, mtime)across a copied tree now sees the
same answer on every version. - A sync that changes nothing stats each root once and lists each directory
once.PathSyncerstat'd every source entry (with the default
follow_symlinks=True, although the listing already carried the stat) and
every target entry one by one, and withremove_missing=Truelisted each
target directory and stat'd its entries a second time: 205 entries in 6
directories cost 206 source and 206 to 411 targetstat()calls. The
listing's stat now answers for every entry that is not a link; a link, an
entry whose listing stat isNone(unknown) and a target name the listing
did not report are stat'd, one call each. On a remote scheme each of those
calls was a request. A backend that does not override_scandir()(the
default stats every entry itself) now stats the entries of each target
directory it syncs into once, instead of each source entry. With
follow_symlinks=Trueandremove_missing=Truethe target link that is
removed is reported tohookandignore_errorby its own stat, not by the
stat of what it points at, as it already was withfollow_symlinks=False. - The default
PathSyncerchecksum asks for a native digest only when both
sides can give one. For a source with a server-side digest and a target
without (LocalPath,MemPath), every compared file computed one digest
that was thrown away before both sides were streamed. Whether a side can
answer is now checked first, and the second side is asked only when the first
answered. - A carried pattern under an extended-length anchor is expanded, and checked
when it is carried.LocalPath(r"\\?\C:\data\*.py").glob(None)selected
nothing (a wildcard-free\\?\C:\data\a.pytoo): the?of the prefix was
read as a wildcard. The anchor is no longer searched for one. A carried
pattern that is invalid (a**before 3.13) now raises whenglob(None)is
called, as a pattern argument does, and not when the result is first
iterated. - An explicit
case_sensitive=changes the case rule and nothing else. On
Windowscase_sensitive=Truemade..select nothing (glob("sub/../*"),
rglob("..")), andcase_sensitive=Falsejoined a literal without looking
for it (glob("SUB")yieldedSUB,glob("nul")andglob(" ")yielded a
path)..and..stay literal, and a literal is looked for in the listing
whenevercase_sensitiveis passed, yielding the name as stored, aspathlib
does. glob()checks a literal component as the runningpathlibdoes. Before
3.13 one that is not the last must be a directory:a.txt/..,missing/..
and**/deep/..selected the base or every directory on 3.9. From 3.12 the
last literal is tested without following a link, soglob("dangling")
selects a dangling symlink, aspathlibdoes there;glob("dang*")already
did. AMemPathfollows the tree for.., which makes the same patterns
select nothing there on every version.rglob(".")selects every entry below. It raisedValueError;pathlib
globs**.glob(".")is still aValueError.glob(on_error=)is told about a missing or non-directory base whatever
the first component is.glob("*")reported it andglob("**")and
glob("**/*")did not. Each failed listing is reported once: a recursive glob
called the hook twice for one unreadable directory.- A recursive glob lists each directory once.
glob("**/*.txt")and
rglob("*")listed every directory twice, once to find the directories below
it and once to match names in it; on a remote scheme each listing is a
request. The first listing now serves both. bound_loops=Truekeeps every directory wherest_inois 0 orNone.
A filesystem that reports a device but no file index gave every directory the
same identity, so each looked like its own ancestor andglob("**/*.conf", bound_loops=True)returned only the top level. Such a path is walked
unbounded, as the documentation says.LocalPath.glob()andrglob()raise the audit eventspathlibraises.
pathlib.Path.globandpathlib.Path.rglobwere never raised for a
LocalPath(os.scandirstill audited each directory). An audit hook
written againstpathlibnow sees the same events with the same arguments
on the running version.glob(None)has nopathlibcounterpart and raises
none.make_archive(format="tar")sizes a member from the bytes it read. The
member size wasstat().st_size, so a file whose backend does not know its
size (an HTTP file served withoutContent-Lengthreports 0) became an empty
member with no error, and a size that was too large failed withunexpected end of data. The content is read once into a buffer, counted, and then
written, so the member holds the file. A tar of a large file is read and
buffered once more than before.make_archivekeeps directories, times and permissions. A source with
empty/andsub/run.sharchived one member, sounpack_archiverebuilt no
empty/; every directory is now a member of its own, before what is in it.
Members were dated 1980-01-01 (zip) or the epoch (tar) whatever the source
said; a member now carries the modification time the source reports, and a
source that reports none keeps those two defaults. A tar member from a source
that reports no permission bits (MemPath, an HTTP file) was read-only
(0o444); it is0o644for a file and0o755for a directory. Code that
unpacks these archives needs no change.unpack_archiveskips a tar link that names itself. A symlinka -> a,
a -> bwithb -> a, or a hard link to itself ended the extraction with
RecursionErrorand no warning. Links are resolved by a loop that follows a
chain once, so such a link is skipped with theUserWarningevery other
unresolvable link gets, the other members are extracted, and a chain of any
length ending at a file is followed.- A host made only of digits is a host.
UriPath("s3://20240101/key")
had host""and rendereds3:/key, so a bucket, container or account named
with digits addressed nothing (gs://123456/k,az://1234567/c/k,
http://12345/xandsftp://u@007/xlikewise). The host is now20240101
and the URI renders as written.http://12345:80/xwas already right. - The first unescaped
:of a userinfo separates user from password.
sftp://us%3Aer:S3CRET@h/pwas read as useruswith passworder:S3CRET
and renderedsftp://us:er:S3CRET@h/p, a different credential from the one
written. The user isus:erand the URI rendersus%3Aer:S3CRET.
Source.userinfois still the decodeduser:passwordtext (astr);
parsed_userinfo()and the composer read the split it recorded, and two
endpoints that differ only in that split no longer share a backend. - A port is 0-65535, and a
:must be followed by digits.http://h:99999/
parsed,http://h:0/xparsed and rendered without:0(and counted as the
same endpoint as no port), a 5000-digit port was an error on 3.14 and a
5000-digit integer on 3.9, andhttp://h:abc/orhttp://h:-1/made the
wholeh:abcthe host name. A port above 65535 and a:followed by
anything but digits (after a name that is not a bracketed address) are now a
ValueErrorthat does not echo the input; an empty port (http://h:/) is
still no port, and port0is kept. ASourcebuilt by hand with a scheme
that is not an RFC 3986 scheme or a port that is not 0-65535 raises
ValueErrorwhen it is rendered, whereSource("http", None, "h", "80/evil")
used to add a path to the URI. - An IPv6 zone follows
%25.http://[fe80::1%25eth0]/xhad zone
25eth0and an unescapedhttp://[fe80::1%eth0]/xwas written back with a
bare%. The zone iseth0, and the URI is written[fe80::1%25eth0]
whichever spelling it was read from. A zone that was spelled%25followed
by nothing is now an error. Source.as_str()andstr(Source)compose likeUri.as_uri(). They went
through a second composer: a non-ASCII host came out percent-encoded
(http://b%C3%BCcher.example, nowxn--bcher-kva), an empty host gave
file://andx://(nowfile:andx:), and a scheme or port that was
not valid raised from one composer and not from the other.Source.from_str()does not echo its input. A path, query or fragment
after the authority raisedValueErrorwith the whole string, password
included; the message now names the component (found a path). A port out of
range does the same.repr()andstr()of aSourcealso hide the whole
userinfo of agithub:,gitlab:orgit:source, where the token is the
user name and was printed.- A file name holding a lone surrogate can be printed, hashed and sorted. An
NTFS directory may holda\ud800b.txt;iterdir()yielded the child, but
str(),repr(),hash(),==andsorted()of it raised
UnicodeEncodeError. Such a name renders as its UTF-8 bytes
(%ED%A0%80); an escaped non-UTF-8 byte renders as that byte, as before. - A path of a scheme works without its client library, and the error names the
extra.UriPath("http://h/x"),https:,dav:,github:,gitlab:and
git:raisedModuleNotFoundError: requestsat construction,s3:did the
same forbotocore, so such a URL could not even be parsed or joined;gs:
andaz:constructed and failed on the first request withNo module named 'google'. Every scheme class now imports and constructs, joins, compares and
renders without its library, and the first operation that needs it raises
ImportErrorsaying so and naming the extra (pip install "pathlib-next[http]";s3,gsandazlikewise).import pathlib_next.uriwithouturitoolsraises aModuleNotFoundErrorthat names
theuriextra,pathlib_next.UriPathanAttributeErrorthat does, and
hasattr(pathlib_next, "UriPath")staysFalse. A program that caught
ModuleNotFoundErroraround the construction of a path to find out whether a
client is installed now gets theImportErrorat the first operation, and
from pathlib_next.uri.schemes import HttpPathno longer fails without
requests. schemesmap=is an allow-list for every class a path chooses. It was read
once, for the first dispatch:zip:opened its outer archive through the
global registry (sofile:was reachable although the map omitted it),
copy("file:///x")wrote a local file, and joining aUriPathargument,
with_source()and astrdestination chose from every registered class.
The path now keeps the map, and each of those chooses from it (a scheme the
map omits gives a plainUriPath, whose I/O raisesNotImplementedError);
an archive path does not reuse a handle another path opened through
file:. The map restricts dispatch only: it does not stop code that holds a
built path or class from using it.- A pickled
UriPathholds its URI and nothing else. The pickle of a path
that had a backend carried it: the URI password, the session headers of
with_session(), S3client_kwargsand an SFTPconnect_optspassword were
all in the bytes, azip:path did not pickle at all, and ans3:orgs:
path stopped pickling once its client existed. A path now pickles as its URI
text (a URI with userinfo pickles with it, since that is the URI) and its
schemesmap; the stat hint is not part of it, a backend the path built for
itself is built again when needed, and a backend you supplied is dropped
unless its class setspicklable = True. A program that
sent a path with a supplied backend to another process and relied on the
backend arriving must setpicklable = Trueon the backend class or call
with_backend()on the unpickled path.copy.copy()andcopy.deepcopy()
share the backend object (supplied or built by the path) instead of copying a
connection, so a deep copy (dataclasses.asdict()makes one) no longer fails
on azip:path. Source.is_local()answers for the host alone and forgets. It cached per
wholeSource, password included, so three paths differing only in password
cost three lookups and the cache kept theSourceobjects (and passwords)
alive for the life of the process; an answer never changed even if an
interface came or went. The answer is now kept per host for 60 seconds, for at
most 256 hosts.- A query built from a mapping or pairs escapes
+and=.Query({"sig": "ab+cd=="})wassig=ab+cd==, which a form-style reader on the server takes
asab cd==; a base64 signature or an ISO-8601 offset (+00:00) reached it
altered. It is nowsig=ab%2Bcd%3D%3D, anddecode()returns exactly what
the encoder was given for any text. Astris still kept byte for byte, and
decode()still follows RFC 3986: a+in received text stays a plus
(urllib.parse.parse_qslreads form-encoded text as spaces). A program that
compared the encoded text of a query holding+or=in a value must expect
the escapes. - A
data:payload may hold an unescaped?.UriPath("data:,a?b")read
a, and an SVG payload that starts<?xmlread<, with the rest filed as
the URI's query;stat().st_sizewas short too. RFC 2397 has no query, so
the query is part of the payload (decoded like the path) and the full text is
read. A#still starts the fragment. with_path()andwith_segments()build a URI that prints. On a URI with
an authoritywith_path("y")andwith_segments("y")returned a path
without its leading/, andrepr(),hash()and==of the result raised
ValueError; the slash is now added as the constructor adds it.with_segments()
andwith_path()acceptbytes,PurePathandos.PathLiketoo, so
samefile(pathlib.Path(...))no longer raisesTypeError. Astrgiven to
samefile()is still a path, not URI text.Uri.queryis always aQueryandfragmentalways astr.
Uri("http://h/x").querywasNone(so.query.to_dict()raised
AttributeError) while the same URI joined with a name had""; both are
now an emptyQueryand"".repr()of a URI whose parts were never read
showed<pathlib_next.uri.Uri object at 0x...>; it shows the URI. An
explicit second initialization of a URI with other parts (with_source()
does one) leftas_uri()andnormalized_pathof the old parts in their
caches, and one with the same parts reset the caches another thread had
filled; the first now clears every cache and the second none.Sourceslices andSource.from_str("file:")equals what a parse gives.
source[0:2]raisedTypeError; it is the tuple slice.from_str("file:")
had hostNonewhereUri("file:").sourcehas"", so the two compared
unequal.- A
strjoin keeps an empty segment inside its key.Uri("http://h/d") / "x//y"washttp://h/d/x/y, whileUri("http://h/d") / Uri("x//y"),
Uri("http://h/d", "x//y")andUri("http://h/d/x//y")were all
http://h/d/x//y, so on an object store a key holding//could not be
addressed with/orjoinpath(). The four spellings now give the same
URI (http://h/d/x//y), and so does astrdestination ofrename(),
copy()andmove().normalized_pathstill collapses, and a name a
listing yields as empty is still skipped. Code that relied onp / "a//b"
collapsing, aspathlibdoes, strips the empty segment itself. - The ancestors of a UNC
FileUristop at the share root. On Windows
UriPath("file://server/share/a/b").parentsendedfile://server/shareand
file://server/, which names no file system, andparentof the share root
climbed out of the share. The share is the anchor, as it is for
PureWindowsPath: the ancestors arefile://server/share/aand
file://server/share/, and the share root is its own parent. Off Windows
the root of the host still ends the chain. - A
FileUriargument holding a drive restarts a join on Windows.
UriPath("file:///C:/d/") / UriPath("file:///D:/f")wasfile:/C:/d/D:/f
(the same join with a plainUri, or with thestr"D:/f", was already
file:/D:/f). The argument now restarts the join whichever class carries it. - An error raised while a join is built is reported as that error.
Uri("x") / otheranswered "unsupported operand type(s) for /" for any
TypeErrororNotImplementedErrorraised anywhere in the construction of the
result, a bug in a scheme's__init__included;UriPathalready told the two
apart. Only an argument the constructor cannot read is unsupported now, for
both classes, and/,joinpath()and"prefix" / uriare defined once. - A cut-short, stalled or undecodable
http:directory index raises instead
of listing part of it.iterdir(),walk()andglob()read the index
withrequests' own.text: on urllib3 1.26 (what a Python 3.9 install with
thes3extra gets) a connection that closed early gave the entries that had
arrived, whichPathSyncer(remove_missing=True)read as the whole directory
and deleted the rest; a stalled body raisedrequests'ConnectionError
rather thanTimeoutError, and a badContent-Encodingleakedrequests'
ContentDecodingError. The index is now read the wayopen()reads a file:
a short body isOSError(EIO) and a stall isTimeoutError, both naming
the directory. - A DAV listing keeps only the members of the collection it asked about.
APROPFINDreply that held members deeper thanDepth: 1, hrefs for
another path, another host or the collection spelled differently (a proxy
that strips a prefix) listed every one of them as a child by the last
segment of its href, so the directory could list itself; an href of""or
/made the whole listing raiseNotADirectoryError; and any well-formed
XML that was not amultistatus(a login page answered with 200) listed as
an empty directory andstat()ed as missing. A member is now listed only
when its href, resolved against the collection, names the same host and a
direct child; a reply that is not amultistatus, or names neither the
collection nor any member of it, isOSError(EIO). - A size a server states that is not a plain number is unknown.
Content-Length: abcmadeHttpPath.stat()raiseValueError, one member
with<getcontentlength>abc</...>madeDavPath.iterdir()of the whole
collection raise it, and a negative number became a negativest_size. Each
is now an unknown size (st_size0, and patch-mode append still refuses a
HEADwithout a usableContent-Length). - A listed
http:/dav:name that is not UTF-8 can be opened.caf%E9.txt
listed with U+FFFD in place of the byte and the request for it named
caf%EF%BF%BD.txt.
The name keeps its bytes asUri("http://h/caf%E9.txt")does and the request
names the file the server listed. - An
http:directory URL with a query or fragment is a directory.
http://h/d/?C=Mandhttp://h/d/#topread as files, sounlink()sent the
DELETEit refuses forhttp://h/d/. The path of the final URL decides. - A
PROPFINDbehind a redirect keeps its method and body.requests
followed a 302 or 303 as aGET(sostat(),exists()and listing of a
dav:URL behind one failed or read as missing) and a 301 without the body
(the server saw anallproprequest). APROPFINDis now sent again, with
its headers and body, after a 301, 302, 303, 307 or 308 to the same scheme,
host and port, and refused withOSError(EIO) naming the status and the
Locationfor another origin, a second redirect or any other 3xx. Adav:
URL that redirects to another origin (http:tohttps:, say) must be
spelled with its final scheme and host (davs:). - URL passwords outside Latin-1 are sent as the octets the URL carried.
http://user:caf%C3%A9@host/went out as the single byte E9 (requests
encodes a text credential as Latin-1) and%FFraisedUnicodeEncodeError
fromstat()before anything was sent, soexists()saidFalse. The
percent-escapes are now decoded to bytes andrequestssends those bytes;
an ASCII credential is passed as text as before. as_posix()of agithub:,gitlab:orgit:path no longer shows the
userinfo.str(),repr()andas_uri(sanitize=True)dropped it but
as_posix()renderedTOKEN@github.com:/o/r/f. It isgithub.com:/o/r/f
now;as_uri()still round-trips the userinfo.- A
github:path cannot add to its API URL. Owner and repository were put
into the URL raw (github://github.com/o/r%3Fx=1/a.txtasked for
/repos/o/r?x=1/contents/a.txt) and a..segment made withwith_name("..")
orwith_path()was collapsed by the client into another repository's
address, with the token attached. Owner, repository and every path segment
are percent-encoded now, and a.or..segment in the path or the ref
raisesValueError(exists()answersFalse); the same check covers
gitlab:'s file path. open("r+"),open("r+b")onhttp:anddav:raise
NotImplementedError. They returned a read-only stream whose first write
failed. Open with"rb"to read. Thegithub:andgitlab:paths already
refused them.dav:uploads with thewrite_methodgiven towith_session(). It was
accepted and ignored (PUTalways went out).DavPath.stat()also accepts
walk_up_last_modified=likeHttpPath.stat(); the PROPFIND reply already
carries the modification time, so it changes nothing.- A
github:orgitlab:API reply that is not the documented shape is
OSError(EIO) naming the path. A 200 that was a proxy's HTML page,
null, a number, a truncated body, an array where an object belongs or an
entry without a name and a type raisedAttributeError,TypeError,
KeyErrororrequests'JSONDecodeErrorfromstat(),iterdir()and
read_bytes(), andexists()raised too (it only swallowsOSError); a
tree reply without atreelisted as an empty directory, and a size that
was text brokestat(). Each reply is decoded and checked in one place per
provider now,exists()answersFalseandstat()raisesOSError. A
raw file body has no shape to check and is returned as sent. DavPath.rename()no longer puts the source's query inDestination. A
source such asdav://h/d/f.txt?sig=...sentDestination: .../d/g.txt?sig=...,
handing a signature meant for the source to the destination URL. The header
carries the target's own query (none for astrtarget) and no fragment, and
rename()returns the target path with that query. Pass aUritarget that
carries the query a server needs on the destination.- A lost connection, a refused login and an untrusted host key are
OSError,
on both SFTP backends.asyncssh.ConnectionLost,HostKeyNotVerifiableand
PermissionDenied,paramiko.AuthenticationException,BadHostKeyException
and theSSHExceptionof a rejected key crossed the API, and a translated
server status was chained to the library's exception. The dropped connection
is nowConnectionResetError, the loginSftpAuthenticationError, the key
SftpHostKeyError, any other failure of the handshakeConnectionAbortedError
and a refused connectionConnectionRefusedErroron both; nothing is chained
(the library's text can carry credentials). A caller that caught
paramiko.AuthenticationExceptionorasyncssh.PermissionDeniedcatches
SftpAuthenticationErrororPermissionError; one that caught
paramiko.SSHExceptionfor a refused key catchesSftpHostKeyError. After a
lost connection no second request is made to find out whether the entry
exists (mkdir()of a directory that was created could answer
FileExistsError). - A directory holding a name that is not UTF-8 lists on both SFTP backends.
With paramiko one such name raisedUnicodeDecodeErrorout ofiterdir(),
walk(),glob()andcopy(); with asyncssh it raisedOSError, which
walk()andglob()swallowed into an empty result, andrm(recursive=True)
could not remove the tree. The name is now the lone-surrogate string of its
bytes, beside its siblings, and reads, stats, unlinks and copies by the same
bytes. - paramiko's SFTP errors agree with asyncssh's. An "operation unsupported"
status was anOSErrorwithout anerrno(socopy()to a server that
refusesSETSTATraised after the data had been copied and a recursive copy
stopped at the first file); it isNotImplementedError, whichcopy()
tolerates. Errors now carry the remote path infilename(andfilename2for
rename()), and the two-path error of asyncssh no longer prints
[WinError None]on Windows. chown(None, gid)andchown(uid, None)onsftp:never send 0 for the id
they leave alone. An SFTP version 4 server (asyncssh asks for 4 by default)
names the owner and the group instead of numbering them, so every file read
as owned by 0 andchown(None, 4242)asked the server to give the file to
root. An owner name that is a number is that id; any other name makes the
partialchown()raiseNotImplementedErrornaming the field, with nothing
sent.chown(uid, gid)is unchanged, and so is a version 3 server's (OpenSSH).- A pickled or deep-copied
sftp:backend works. The defaultssh_config
and, for paramiko, the defaultknown_hostswere bare objects compared by
identity, so a copy was read as "an iterable of paths" and the first request
raisedTypeError. They are the same object after a pickle or a copy. A
path pickled with abackend=still carries only its URI andssh_config. SftpPath.with_source()carries the path'sssh_config, and a join onto
another host does not.with_source()handed the default back (a path told
to read no configuration read~/.ssh/configon the new host), and
SftpPath(a, "sftp://other/y")tooka'sssh_configontoother. The
configuration now follows a derivation on the same endpoint and is dropped,
with the backend, on another host. Two paths of one endpoint whose
ssh_configdiffers no longer share the backend they build for themselves.checksum()onsftp:asks per algorithm. A server that refused one
algorithm with "operation unsupported" (a FIPS build and md5), a bare failure
for one file, or a probe bysupported_checksums()(md5 only) switched native
checksums off for the whole connection, and every later digest was streamed.
Only the algorithm refused is remembered now, a failure for one file refuses
nothing, andsupported_checksums()is never empty for a connection that
produced a digest. An algorithm other thanmd5,sha1,sha224,
sha256,sha384,sha512andcrc32isNotImplementedErrorwithout a
request, and a reply is a digest only of the size its algorithm has (an empty
reply was returned as''forcrc32).- File objects of the two SFTP backends agree.
write_bytes()and
open("xb").write()returnedNoneon paramiko; they return the count, as on
asyncssh.truncate()raisedio.UnsupportedOperationon asyncssh and
required a size on paramiko; both truncate, andtruncate()without a size
cuts at the current position.fileno()raisesio.UnsupportedOperationon
both. - A file left open at exit is written.
path.open("w").write(...)without
close()at module scope left an empty file on asyncssh (and, for text, on
paramiko) with aRuntimeErrorabout shutting down. Files opened for writing
are flushed and closed when the interpreter exits, before its threads stop,
waiting at most five seconds for a connection that has hung. - A URI user cannot add arguments to an asyncssh
ProxyCommand. A user such
asa -oProxyCommand=calcwas expanded into%rand split afterwards, so
the part after the space became an argument of the proxy program (paramiko
already refused it). The same users are refused withValueErrorbefore
asyncssh is given the user, when the configuration for the host would pass it
on; with no%rin theProxyCommand(or no configuration) a user with a
space still connects. - Testing the children of a listed
sftp:directory costs no request per
child.[c for c in d.iterdir() if c.is_dir()]made one listing and one
statper entry, because the listing's attributes were used only for
lstat(). A listed entry that is not a link now answersstat(),
is_dir(),is_file()andexists()from the listing (once, as before), so
21 entries cost one request; a link is still followed by a request of its
own. The asyncsshcopy(recursive=True)andrm(recursive=True)also decide
file or directory from the listing: removing 128 files in 8 directories
made 301 requests and now makes 165, the number the genericrm()makes. copy()andrm()on the paramiko backend do not import asyncssh. Both
imported it before looking at the backend, so forcing
PATHLIB_NEXT_SFTP_BACKEND=paramikostill loaded it on the firstcopy()
orrm(), and without it installed the failing import was retried on every
call. A backend now says whether it has native tree operations
(BaseSftpBackend.supports_tree, withtree_copy()andtree_rm(); False
unless a backend sets it); a custom backend needs no change.- A paramiko connection of a thread that has ended is closed. Entries were
closed only when the 128-entry cache evicted them, so a program that used
SFTP from many short-lived threads held a transport thread and a socket for
each until then. The next new connection closes the ones whose thread has
ended. - An ssh_config that was edited is read again by the paramiko backend. The
parsed file was cached by its path for the life of the process, so a changed
Hostblock was ignored until restart. The cache now also keys on the file's
modification time and size (a change to anIncluded file alone is not
noticed). - An
archive:path reads nothing until it is used.UriPath("archive:http://h/noext!/m")
sent a request while it was being built (an outer whose name does not say
zip or tar is sniffed), a second one to read the member, and a sniff that
failed made that path object a tar for good, so a good zip then failed with
tarfile.ReadError. The format is now settled on first use: building,
printing, joining, copying and pickling a path send nothing, a non-local
outer read to decide is not fetched again, and a failure to read the outer
propagates and decides nothing, so the next call tries again. A write
through anarchive:path whose outer is missing and has no extension is now
FileNotFoundError(it wasNotImplementedError). - A tar member whose name is not UTF-8 can be printed, hashed and compared.
A tar written in a Latin-1 locale listscaf\udce9.txt, andstr(),
repr(),hash(),==andas_uri()of that path raised
UnicodeEncodeError. The name now keeps its bytes as%XXin the URI
(.../caf%E9.txt), as every other scheme composes it, and the URI parses
back to the same member. - A damaged archive raises one exception family per format. Besides
zipfile.BadZipFileandtarfile.ReadError,exists(),stat(),
iterdir()andread_bytes()leakedzlib.error,EOFError,
NotImplementedError("zip file version ...")andgzip.BadGzipFile(an
OSError, soexists()answeredFalsefor a corrupt.tar.gz). They are
nowBadZipFilefor a zip andReadErrorfor a tar, with the decoder's error
as__cause__. Code that caughtzlib.errororEOFErroraround a read of
an archive member catches those two instead. An encrypted member is still
RuntimeErrorand an unsupported compression method
NotImplementedError. exists()andis_file()of a zip member no longer raiseKeyError
while another thread rewrites the zip. The name was looked up in one
snapshot and its stat read from a handle that had been reopened in between;
both now happen under one hold of the handle's lock, and a member that is
gone by then isFileNotFoundError.- New members written through
zip:are deflated. A member added with
write_bytes()oropen("w")was stored uncompressed, so 100 000 bytes of
one letter took 100 000 bytes. An overwritten member keeps the compression
method it had (a storedmimetypestays stored). - A read-only zip is not replaced on POSIX.
write_bytes(),unlink()and
rename()on a zip whose mode forbids writing succeeded, because the archive
is replaced by a new file; they raisePermissionErrorbefore anything is
written, as on Windows. Replacing the archive still gives it a new inode:
another hard link to it keeps the old content. stat()onftp:is one reply where the server offersMLST, and a
connection is probed withNOOPonly after it has been idle. A stat sent
NOOP,TYPE,PASVandMLSDof the parent directory over a new data
connection (the whole listing, so N stats in a directory of N entries were
quadratic), andexists()of a missing file sent 10 commands. A server whose
FEATlistsMLST(RFC 3659) is now askedMLSTof the entry itself, a
single reply on the control connection;exists()of a missing file is
MLST,TYPE,SIZE,CWD. A connection that carried a command within
schemes.ftp.IDLE_PROBE_SECONDS(1 second) is used without aNOOP; one that
has been idle longer, or has not been used yet, is probed and replaced if the
server dropped it, as before. A server withoutMLSTis read as before: the
parent'sMLSD, thenSIZE/CWD.stat(),exists()andis_file()of anftp:file work when its
directory holds a name the client cannot decode. The parent's listing
failed withOSError(EILSEQ)for every call and each call reconnected. With
MLSTno listing is read; on a server withMLSDand noMLSTthe error
makesSIZE/CWDanswer (the listing still drops the connection, so each
such stat reconnects).iterdir()of that directory still raises
OSError(EILSEQ).- A malformed
sizefact no longer makes a directory unlistable.size=abc
orsize=1.5raisedValueErrorfromiterdir(), a negative size was
reported as is, and a size of 400 digits became anint. Asizethat is not
a plain non-negative decimal below 2**63 is unknown (st_sizeis 0), as a
missing one is. rename()onftp:onto an existing target isFileExistsError. A server
that does not replace it (a Windows one answers550 File exists) raised
PermissionErrorwith both files intact. The refusal isFileExistsError
when the target is there, andPermissionErrorstill when the reply names a
permission problem or the target is not there. A POSIX server replaces the
target, as before.- The connections of a thread that has ended are closed. Twenty short-lived
threads that each used oneftp:path left twenty logged-in control
connections open until 128 newer ones pushed them out, which a server's
per-client connection limit can refuse. A thread's connections are now closed
when the thread ends, and one that another thread's cache overflow drops while
a command is running on it is closed when that command ends. - A
chmod()the FTP server refuses says why. A refusedSITE CHMOD
("550 Not enough privileges") raisedNotImplementedError("SITE CHMOD not supported by this server")without the reply. It is still
NotImplementedError(socopy()skips it), now with the server's reply in
the message and as__cause__. - Walking, listing and
stat()of an archive no longer scan every name for
each directory.iterdir()of a directory compared every member name of the
archive against the directory's prefix and thenstat()-ed each child with a
second validation of the file, so awalk()grew with directories times
members. The members, their directories (those that exist only through their members
too) and each entry's stat data are derived once per open handle; a listing
costs one validation of the file and work in the size of the directory, and
the childreniterdir()returns answer their firststat()from it. - Reading a member of an archive holds a bounded amount of memory. A local
tar:was read whole into memory and kept for the life of the handle, and a
member read built the whole member in memory at about twice its size (a zip
member that declares 512 MiB peaked at over 1 GiB). A local tar is now read
from its file as members are reached, and a member larger than
schemes.archive._base.MEMBER_SPOOL_BYTES(16 MiB) is copied to an unnamed
temporary file instead of memory, so reading it costs disk in the size the
archive declares. A non-local archive is still read whole into memory. Code
that read a member throughopen("rb")is unchanged: the stream is still
seekable and independent of the archive. - A zip change copies the entries it does not change as they lie in the
archive.unlink(),rmdir(),rename(),rm(recursive=True)and an
overwrite decompressed and compressed every member of the archive again, so
their cost was the archive's uncompressed size, and one encrypted member (or
one stored with a compression methodzipfilelacks) made every such change
to the archive fail withRuntimeErrororNotImplementedError. Untouched
entries are copied without being decompressed, a renamed entry gets a new
local header and its data is copied, and a member with a non-ASCII name that
the archive stores without the UTF-8 flag gets the flag, as before. stat()of a tar hard link reports the size of its target however the
target is spelled. A link tod//real.txtord/./real.txtreported
size 0 while reading it returned the data; tar and zip now use the one name
rule.archive+zip:andarchive+tar:are second names ofzip:andtar:.
Both schemes, both class names (ArchiveZipUri,ArchiveTarUri, now aliases
ofZipUriandTarUri) and the printed and pickled form of a path (it keeps
the scheme it was written with) are unchanged.type(path)of an
archive+zip:path isZipUri, sorepr()shows that name, and
isinstance(path, ArchiveZipUri)is also true for azip:path.- A failed request on
s3:,gs:andaz:is anOSErrorof the type the
header promises, and the SDK's exception is not chained. A timeout was
OSError(EIO)ons3:; an unreachable endpoint reachedexists()and
walk(on_error=)as the SDK's own exception ongs:andaz:(after up to
two minutes with the SDKs' default retries), so neither saw it. A request that
runs out of time is nowTimeoutError, an endpoint that cannot be reached
ConnectionError, a connection that breaks while an answer or a body arrives
ConnectionResetError, each with the path asfilename; the store's answers
stayFileNotFoundError,PermissionErrorandOSError(EIO). The message
names the SDK exception's type and not its text, which holds the request URL
and with it any query-string credential, and__cause__isNone. Catch
OSErroror these subclasses; code that read the SDK exception from
__cause__must catch it from the SDK call itself. - An
s3:read cut short or stalled in the body is anOSError. Only the
call that opens the object was translated: a server that closed the
connection mid-body raised botocore'sResponseStreamingErrorfrom
read_bytes()andcopy(), and a stalled body botocore's
ReadTimeoutErrorwithout the path. They areConnectionResetErrorand
TimeoutErrornaming the path. - Reading or writing a bucket or container root, and a path with no bucket or
account.read_bytes(),write_bytes()oropen("r+")ofs3://bucket/
raised an SDK error (botocore'sParamValidationError, aValueErroror an
HTTP 400 reply), andS3Path("s3:x").exists()raised the first too. A root is a directory
(IsADirectoryError, nothing is sent), and a path with no bucket, or no
account and nobackend=, does not exist (FileNotFoundError,exists()is
False). az://accountcan be listed.is_dir()answeredTruefor it while
iterdir()andwalk()raisedValueError; its children are the containers.rm(recursive=True, missing_ok=True)under a missing bucket or container
returns. It raisedFileNotFoundError(a missing key in an existing
bucket was already fine).rename()of a missing path onto its own name returned the path; it
isFileNotFoundError, and renaming an existing key or prefix onto itself
still changes nothing.- A key a batch delete refuses is offered to
ignore_errorwith its own
path. Ons3:the refused keys of a 1000-key batch reachedignore_error
as oneOSErrorwithout anerrno, with the directory being removed as the
path; each is now aPermissionError(AccessDenied) orOSError(EIO)for
its key, offered once with that key's path, and without a handler the first is
raised.gs:andaz:offered a failed delete with the directory's path too. - An
S3Backend,GsBackendorAzBackendbuilds one client however many
threads ask first, and can be pickled or copied after it built one. Threads
racing for the first request each built a client (and, on Azure, a
credential). A backend that had made its client could not be pickled or
deep-copied (PicklingErrorfor boto3's and Google's clients, a
RecursionErrorincopy.deepcopy). A pickled or copied backend keeps its
options and builds its own client on first use. A pickledS3Path,GsPath
orAzPathstill holds only its URI: the receiving process uses the default
backend unless the program supplies one, and noclient_kwargs, credential or
client is in the bytes. - Existence probes on
az:ask the service for one item.stat()of a
prefix,exists()of a missing path,mkdir(),open("x"), writes and
rmdir()listed with the service's default page (up to 5000 entries) to learn
whether one blob exists.rename()of a prefix directory on all three stores
asked for the key a second time before listing the prefix. AzPath.rename()no longer waits for ever on a pending copy. It waits at
mostCOPY_POLL_TIMEOUT, aborts the copy and raisesTimeoutError, the source
untouched.uripathexits 141 when the reader of stdout went away, on Windows too.
Windows reports a closed pipe asOSError(EINVAL), soread,writeand
syncprinteduripath: OSError: [Errno 22] Invalid argumentand exited 1 or
120 unless the reader left in the middle of a large write. They now exit 141
without a message, as the documentation says.- Without the
uriextra,uripathno longer echoes a URI argument. The
error line quoted the whole argument, password included. It now readsthe 'http' scheme needs the 'uri' extra: pip install 'pathlib-next[uri]'. uripathreports a wrong invocation with exit status 2 and the usage.
cp -rwith-and an unknown--encodingexited 1 as if the operation had
failed. Every flag and positional now has a help line.uripath.main(stderr=)accepts a binary stream. The error line was written
as text, so aBytesIOraisedTypeErrorinstead of returning 1.
Full Changelog: v0.9.12...v0.9.13