Protocols API
The small protocols Path is built from. A class implements the primitives of
the protocols it lists (stat, _open, iterdir, unlink, readlink, ...) and
inherits everything derived from them; supports() and capabilities() say which
primitives a class has.
pathapi.protocols
supports(obj, *protocols)
Whether obj (an instance or a class) implements every protocol
named: each primitive of each resolves to something other than the
protocol's default stub. A class whose answer depends on its connection
defines _supports(protocol) -> bool | None, asked first for an instance;
None falls through to the answer from the class. That hook makes at most
one request per backend, cached for the backend's lifetime, and never
raises: a failed request answers None.
The answer is advisory, like supported_checksums(): a primitive may
still raise NotImplementedError for an argument it cannot handle.
TypeError for no protocol, or for something that is not a protocol of
this package.
Source code in src/pathapi/protocols/_support.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
capabilities(obj)
The protocols of this package that obj (an instance or a class)
supports, in the sense of supports().
Source code in src/pathapi/protocols/_support.py
113 114 115 116 117 118 119 120 121 122 | |
pathapi.protocols.pure
What the helpers that walk, match and sync need from a pure path.
__all__ = ['Named']
module-attribute
Named
Bases: Protocol
The pure half of a path as glob, walk and sync read it: a name,
the segments it is made of, a parent, and the means of building and
joining paths of the same kind. Structural only: Pathname and
pathlib.PurePath satisfy it without naming it, and nothing subclasses it.
name
property
parent
property
segments
property
__truediv__(key)
Source code in src/pathapi/protocols/pure.py
29 | |
as_posix()
Source code in src/pathapi/protocols/pure.py
31 | |
joinpath(*args)
Source code in src/pathapi/protocols/pure.py
27 | |
with_segments(*segments)
Source code in src/pathapi/protocols/pure.py
25 | |
pathapi.protocols.fs
__all__ = ['Chmod', 'Chown', 'Create', 'FileStatLike', 'Owned', 'Stat', 'Utime']
module-attribute
Chmod
Changing the permissions of a path. It also inherits Chown and
Utime, which it documented before they were separate protocols, so
chown(), _chown() and _utime() stay reachable from it;
supports(x, Chmod) asks for chmod() only.
__primitives__ = ('chmod',)
class-attribute
__slots__ = ()
class-attribute
instance-attribute
chmod(mode, *, follow_symlinks=True)
Change the permissions of the path, like os.chmod().
Extension: mode may be a str, which is parsed as octal
("0755", "755" and 0o755 all mean the same thing) -- see
providers.parse_mode() for why the base is never left implicit.
Every implementation normalizes its argument through
providers.parse_mode() on entry. Unlike symlink_to/chown, chmod is
overridden directly by each backend (each has real per-scheme logic
-- version shims, capability gates, a SITE CHMOD command), so
there is no single wrapper to hang the conversion on; the shared
helper is what keeps the base from drifting between them.
Source code in src/pathapi/protocols/fs.py
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 | |
lchmod(mode)
Like chmod(), except if the path points to a symlink, the symlink's permissions are changed, rather than its target's.
Source code in src/pathapi/protocols/fs.py
324 325 326 327 328 329 | |
Chown
Bases: Protocol
Changing who owns a path: _chown() is the primitive and chown()
normalizes its arguments.
__primitives__ = (('_chown', 'chown'),)
class-attribute
__slots__ = ()
class-attribute
instance-attribute
chown(uid=None, gid=None, *, follow_symlinks=True)
Change the owner and/or group of the path.
Extension: pathlib.Path has owner()/group() readers but no
writer (the stdlib equivalent is shutil.chown/os.chown), so
ownership was the one attribute stat() could report -- via
st_uid/st_gid -- and nothing could write back.
None (the default) leaves a field unchanged, which is what lets a
caller set the group without knowing the owner. -1 is accepted as
an alias for None, matching os.chown's own sentinel. An int is
a uid/gid; a str is a user/group name, passed through for backends
that can resolve one.
Backends implement _chown() and receive an already-canonical pair
(see providers.parse_owner()), so the "unchanged" semantics cannot drift
between schemes.
Source code in src/pathapi/protocols/fs.py
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | |
Create
Bases: Stat, Chmod, Utime, Protocol
Creating a directory or a file: mkdir() and touch() on top of the
_mkdir() primitive. touch() also uses open() (BinaryOpen) and
unlink() (Remove).
__primitives__ = (('_mkdir', 'mkdir'),)
class-attribute
__slots__ = ()
class-attribute
instance-attribute
mkdir(mode=511, parents=False, exist_ok=False)
Create a new directory at this given path.
Source code in src/pathapi/protocols/fs.py
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 | |
touch(mode=None, exist_ok=True)
Create this file, if it doesn't exist.
Raises FileExistsError if exist_ok is False and the file already exists (pathlib parity). An existing file is never truncated.
Differences from pathlib.Path.touch, which creates through
os.open() (so the process umask applies) and bumps an existing
file's mtime:
modeis applied withchmod()only when passed explicitly, and then verbatim: a remote server's umask is unknowable. A backend that cannot chmod raisesNotImplementedErrorand the file it created is removed again, so a mode that was asked for is never silently dropped. The default (None) leaves a new file with the permissions the backend gives it, instead of chmod'ing it to a world-writable 0o666.- An existing file's access and modification times are set to the
current time through
_utime(); a backend that cannot set a time leaves the file as it is, since rewriting the content to bump it is not a touch.LocalPathuses pathlib's own touch.
Source code in src/pathapi/protocols/fs.py
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 | |
FileStatLike
Bases: Protocol
Minimum properties stat like object should provide
__slots__ = ()
class-attribute
instance-attribute
st_mode
abstractmethod
property
st_mtime
abstractmethod
property
st_size
abstractmethod
property
Owned
Bases: Stat, Protocol
Reading who owns a path. owner() and group() map the st_uid and
st_gid of stat() through the primitives _user_name() and
_group_name(); a class that has owner() and group() from somewhere
else (pathlib) needs neither.
__primitives__ = (('_user_name', 'owner'), ('_group_name', 'group'))
class-attribute
__slots__ = ()
class-attribute
instance-attribute
group(*, follow_symlinks=True)
The name of the group that owns the path. NotImplementedError
when stat() carries no st_gid or the class does not map group ids
to names (_group_name()).
Source code in src/pathapi/protocols/fs.py
198 199 200 201 202 203 204 205 | |
owner(*, follow_symlinks=True)
The name of the user that owns the path. NotImplementedError
when stat() carries no st_uid or the class does not map user ids
to names (_user_name()).
Source code in src/pathapi/protocols/fs.py
189 190 191 192 193 194 195 196 | |
Stat
Bases: Protocol
Any object that can implement Stat and utilities functions based on it
__primitives__ = ('stat',)
class-attribute
__slots__ = ()
class-attribute
instance-attribute
info
property
What this path is, as an object shaped like pathlib.types.PathInfo
(Python 3.14): exists(), is_dir(), is_file() (each with
follow_symlinks=True) and is_symlink(). It asks stat() at most
once with and once without following links and remembers the answers
on that object only: a new info is a new question. LocalPath has
pathlib's own on 3.14.
exists(*, follow_symlinks=True)
Whether this path exists.
Source code in src/pathapi/protocols/fs.py
68 69 70 71 72 | |
is_block_device()
Whether this path is a block device.
Source code in src/pathapi/protocols/fs.py
94 95 96 97 98 | |
is_char_device()
Whether this path is a character device.
Source code in src/pathapi/protocols/fs.py
100 101 102 103 104 | |
is_dir(*, follow_symlinks=True)
Whether this path is a directory. follow_symlinks=False reports a
symlink to a directory as not one (3.13 parity).
Source code in src/pathapi/protocols/fs.py
74 75 76 77 78 79 | |
is_dir_binding()
Whether this directory is another tree's second NAME rather than part of this one: a Windows junction or a mount point (bind or otherwise).
The distinction that matters to anything walking a tree. A symlink
announces itself -- is_symlink() is True and every walker here
already declines to follow one. A binding does not: it looks like an
ordinary directory, and its contents belong to whoever mounted or
junctioned it. rm(recursive=True) removes the binding itself
rather than the contents behind it.
An implementation that cannot answer (stdlib's is_mount() raises
on Windows before 3.12) counts as "not a binding" rather than
breaking the operation that asked.
Source code in src/pathapi/protocols/fs.py
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | |
is_fifo()
Whether this path is a FIFO.
Source code in src/pathapi/protocols/fs.py
106 107 108 109 110 | |
is_file(*, follow_symlinks=True)
Whether this path is a regular file (also True for symlinks pointing
to regular files unless follow_symlinks=False, 3.13 parity).
Source code in src/pathapi/protocols/fs.py
81 82 83 84 85 86 | |
is_junction()
Whether this path is a Windows junction (pathlib 3.12 parity).
A junction is NOT a symlink: it is a second NAME for a directory,
the Windows equivalent of a Linux bind mount. is_symlink() is
False for one, readlink() does not describe it, and a
non-following stat reports a plain directory -- which is exactly why
a symlink check cannot protect a recursive walk from it.
Default False; LocalPath answers for real. A remote
backend has no such concept unless it says otherwise.
Source code in src/pathapi/protocols/fs.py
128 129 130 131 132 133 134 135 136 137 138 139 140 | |
is_mount()
Whether this path is a mount point (pathlib parity).
The POSIX half of the same idea: a bind mount makes one tree visible
under a second name, with no link anywhere to say so. Default False;
LocalPath answers for real.
Source code in src/pathapi/protocols/fs.py
142 143 144 145 146 147 148 149 | |
is_socket()
Whether this path is a socket.
Source code in src/pathapi/protocols/fs.py
112 113 114 115 116 | |
is_symlink()
Whether this path is a symbolic link.
Source code in src/pathapi/protocols/fs.py
88 89 90 91 92 | |
lstat()
Like stat(), except if the path points to a symlink, the symlink's status information is returned, rather than its target's.
Source code in src/pathapi/protocols/fs.py
44 45 46 47 48 49 | |
stat(*, follow_symlinks=True)
Return the stat of the path (a stub: NotImplementedError).
Source code in src/pathapi/protocols/fs.py
39 40 41 42 | |
Utime
Bases: Protocol
Setting the access and modification times of a path.
__primitives__ = ('_utime',)
class-attribute
__slots__ = ()
class-attribute
instance-attribute
pathapi.protocols.io
__all__ = ['BinaryOpen']
module-attribute
BinaryOpen
Bases: Protocol
Protocol for objects that support open->io.IoBase
__primitives__ = (('_open', 'open'),)
class-attribute
__slots__ = ()
class-attribute
instance-attribute
copy(target, *, progress=None, chunk_size=_shutil.COPY_BUFSIZE)
Copy the binary content from this object to a target object.
progress, when given, is called after each chunk is written as
progress(bytes_copied, total_size): bytes_copied increases
monotonically and equals total_size (if known) after the final
call; an empty file gets exactly one call, progress(0, total_size).
total_size is this object's stat().st_size when self also
implements the Stat protocol and stat() succeeds,
otherwise None -- BinaryOpen alone has no size concept.
chunk_size controls how many bytes are read per iteration
(default: shutil.COPY_BUFSIZE). With progress=None (the
default), behavior and bytes-on-wire are identical to before this
was added (a plain shutil.copyfileobj).
Source code in src/pathapi/protocols/io.py
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | |
open(mode='r', buffering=-1, encoding=None, errors=None, newline=None)
Open the a handle to an object that implement io.IOBase
The mode is validated like open() (ValueError for an invalid mode,
or an encoding/errors/newline with a binary one), and _open()
receives it canonical: one of "r"/"w"/"x"/"a", plus "+" if given --
never "b" or "t", so "rt"/"wt" reach every backend as "r"/"w".
A stream that commits on close inherits providers.DiscardOnError, so
that its own __exit__ calls discard() before closing it when the
with block ended in an exception; binary mode returns the stream
_open() gave it as it is. In text mode the wrapper open() makes
does the same for any stream that has a callable discard().
Source code in src/pathapi/protocols/io.py
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | |
read_bytes()
Open in bytes mode, read it, and close the file.
Source code in src/pathapi/protocols/io.py
124 125 126 127 128 129 | |
read_text(encoding=None, errors=None, newline=None)
Open in text mode, read it, and close the file. (newline= is 3.13 parity.)
Source code in src/pathapi/protocols/io.py
131 132 133 134 135 136 137 138 139 140 141 | |
write_bytes(data)
Open in bytes mode, write to it, and close the file.
Source code in src/pathapi/protocols/io.py
143 144 145 146 147 148 149 150 | |
write_text(data, encoding=None, errors=None, newline=None)
Open in text mode, write to it, and close the file.
Source code in src/pathapi/protocols/io.py
152 153 154 155 156 157 158 159 160 161 162 163 | |
pathapi.protocols.tree
Listing a directory and removing what is in it.
__all__ = ['Listing', 'Remove']
module-attribute
Listing
Bases: Stat, Protocol
Listing the entries of a directory: iterdir() is the primitive;
_scandir(), glob(), rglob(), walk() and iteration derive from it.
__primitives__ = ('iterdir',)
class-attribute
__slots__ = ()
class-attribute
instance-attribute
__iter__()
Source code in src/pathapi/protocols/tree.py
29 30 | |
glob(pattern, *, case_sensitive=None, include_hidden=True, recursive=None, dironly=None, recurse_symlinks=False, native=True, on_error=None, bound_loops=False)
Iterate over this subtree and yield all existing files (of any kind, including directories) matching the given relative pattern.
Pathlib semantics: hidden entries are matched (include_hidden=False
filters them out and skips hidden directories), a trailing separator
selects directories only, "" does not descend into a directory
symlink by default, and a trailing "" also selects files on Python 3.13+
(directories only before). A missing or non-directory parent selects
nothing. An empty pattern raises ValueError, an absolute one
NonRelativePatternError (a NotImplementedError and a
ValueError).
recurse_symlinks=True lets "**" descend through symlinks to
directories, as pathlib 3.13 does. Where that would loop it stops: a
link to a directory the descent is already inside (compared by
st_dev/st_ino) is an entry but is not entered, and where the stat
carries no identity no more than 40 links are followed in one
descent. A backend without symlinks is not affected.
pattern=None expands the pattern THIS PATH CARRIES
(LocalPath("/etc/*.conf").glob(None)) instead of applying one to a
directory: the path is split at its first wildcard and globbed from
there. "" still raises, as pathlib does. Every keyword means what
it does for a pattern argument -- recursive=None auto-enables
recursion for a carried "**", and native= applies. include_hidden
keeps THIS method's default (True); the module-level
glob.glob() this delegates to defaults it to False,
stdlib-glob-style, so the two spellings differ on hidden names
unless the keyword is passed.
native=True (the default) follows the running interpreter on the
two rules pathlib changed mid-series: a trailing "/" is ignored
before 3.11, and a component that merely contains "" ("a")
raises ValueError before 3.13. native=False applies one rule on
every version -- trailing "/" always selects directories only, "a**"
is always a plain wildcard -- so a pattern answers the same
everywhere, which is what a cross-backend or cross-version caller
usually wants.
on_error(error) is called when a directory cannot be listed, the
same contract as walk() and os.walk: raising from it propagates
(an unreadable directory becomes an error), returning treats that
directory as empty. Without it such a listing is skipped silently --
pathlib's behaviour, and the reason a config loader could not tell a
missing layer from an unreadable one. error.filename names the
directory even when the backend left it empty.
bound_loops=True skips a directory whose (st_dev, st_ino) is
already on the current descent path, which is what a loop is: that
bounds a Windows junction loop, which no symlink check can see (a
junction reports is_symlink() == False, so recurse_symlinks=False
does not help and pathlib itself loops until the recursion limit). A
directory reachable under two sibling names is entered under both. A
backend whose stat carries no identity is walked unbounded.
A "" component auto-enables recursion. Pass recursive=False
explicitly to treat "" as a plain "*" instead.
Note for remote schemes (http/sftp): a recursive glob walks the
whole remote subtree, one request/roundtrip per directory.
Source code in src/pathapi/protocols/tree.py
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | |
iterdir()
Yield path objects of the directory contents.
The children are yielded in arbitrary order, and the special entries '.' and '..' are not included.
Source code in src/pathapi/protocols/tree.py
32 33 34 35 36 37 38 39 | |
rglob(pattern, *, case_sensitive=None, include_hidden=True, recursive=True, dironly=None, recurse_symlinks=False, native=True, on_error=None, bound_loops=False)
Equivalent to glob(f"**/{pattern}", recursive=True).
pattern=None and native= mean what they do on glob(); with
None this is glob(None, recursive=True), since a carried pattern
brings its own anchor and nothing can be prefixed to it.
Source code in src/pathapi/protocols/tree.py
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | |
walk(top_down=True, on_error=None, follow_symlinks=False)
Walk the directory tree from this directory, similar to os.walk().
Uses _scandir() rather than iterdir() + a stat() per entry --
for schemes whose listing already carries type metadata (HTTP/DAV
indexes, SFTP listdir_attr, FTP MLSD, S3 list pages), this turns a
remote-tree walk from O(entries) round trips into O(dirs). The
pre-seeded stat is trusted only when follow_symlinks is False
(matching walk()'s own default and the lstat-like semantics of
those listing calls); an explicit follow_symlinks=True always
re-stat()s each entry so a symlink is still resolved.
Without follow_symlinks a symlink to a directory is listed in
filenames and not entered, and so is a Windows junction
(is_junction()): like a symlink it is a second NAME for another
tree, which pathlib.Path.walk() does not enter either (a junction
that points at its own parent would otherwise be walked until the
path is too long). Nothing protects a walk with follow_symlinks=True
from a loop.
A listed name that is not one path component ("", ., .., or one
containing / or NUL; see providers.is_safe_child_name()) is left out
of dirnames and filenames. on_error, when given, is called with
a ValueError for it, error.filename naming the directory.
Source code in src/pathapi/protocols/tree.py
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | |
Remove
Bases: Listing, Protocol
Removing a file, an empty directory or a whole tree: unlink() and
rmdir() are the primitives; rm() derives from them and from
_scandir().
__primitives__ = ('unlink', 'rmdir')
class-attribute
__slots__ = ()
class-attribute
instance-attribute
rm(recursive=False, missing_ok=False, ignore_error=False, *, follow_symlinks=False, follow_binds=False)
Remove this file or directory, optionally recursively and ignoring errors.
follow_symlinks applies to a symlink met during a recursive
removal, follow_binds to a binding -- a directory that is another
tree's second NAME rather than part of this one (is_dir_binding():
a Windows junction, a mount point, a bind mount). Both take:
False(default) -- remove the entry itself, never what is behind it:unlink()for a symlink,rmdir()for a binding. This is whatrm -rdoes, and on a live mount thermdir()fails rather than emptying someone else's filesystem.True-- remove the contents behind it, then the entry. What a walker that cannot tell the difference does by accident.None-- leave it in place. The enclosing directory is then not empty, so its own removal reports that.- a callable
policy(path) -> bool | None-- asked per entry with the same three answers, so one tree can keep one mount and follow another.
Path components BEFORE the final one are followed as usual; these
decide what happens to the entry itself, so they apply to the path
rm() is called on as to one met on the way down: a symlink named
directly is removed, emptied first (True) or left (None), and
without recursive it is only ever unlinked. The name matches
stat()/walk()/copy()'s follow_symlinks= rather than
inventing a second vocabulary for the same idea.
A policy that is not True, False, None or a callable raises
ValueError before anything is removed; so does a callable's answer
that is not one of the three, whatever ignore_error says. A listed
name that is not one path component ("", ., .., or one
containing / or NUL; see providers.is_safe_child_name()) is never
joined onto the directory: it raises ValueError, offered to
ignore_error as (error, directory) once per entry, and nothing is
removed for it.
Source code in src/pathapi/protocols/tree.py
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 | |
rmdir()
Remove this directory. The directory must be empty.
Source code in src/pathapi/protocols/tree.py
319 320 321 322 323 324 | |
unlink(missing_ok=False)
Remove this file or link. If the path is a directory, use rmdir() instead.
Source code in src/pathapi/protocols/tree.py
311 312 313 314 315 316 317 | |
pathapi.protocols.links
Symbolic links.
__all__ = ['Hardlink', 'Readlink', 'Symlink']
module-attribute
Hardlink
Bases: Stat, Protocol
Making a path a second name for an existing file: _hardlink_to() is
the primitive; hardlink_to() turns a str target into a path of this
backend.
__primitives__ = (('_hardlink_to', 'hardlink_to'),)
class-attribute
__slots__ = ()
class-attribute
instance-attribute
hardlink_to(target)
Make this path a hard link to target (pathlib 3.10 parity). A
str is a path on this backend.
Source code in src/pathapi/protocols/links.py
130 131 132 133 134 135 | |
Readlink
Bases: Stat, Protocol
Reading a symbolic link: readlink() is the primitive. A read-only
backend (an archive) has this and not Symlink.
__primitives__ = ('readlink',)
class-attribute
__slots__ = ()
class-attribute
instance-attribute
readlink()
The path this symbolic link points to, as a path of this class whose
str() and as_posix() are the link text, unresolved (a stub:
NotImplementedError). OSError(EINVAL) where the path is not a
link.
Source code in src/pathapi/protocols/links.py
22 23 24 25 26 27 28 | |
Symlink
Bases: Remove, Protocol
Making a symbolic link: _symlink_to() is the primitive;
symlink_to() normalizes its target and honours force=.
__primitives__ = (('_symlink_to', 'symlink_to'),)
class-attribute
__slots__ = ()
class-attribute
instance-attribute
symlink_to(target, target_is_directory=False, *, force=False)
Make this path a symlink pointing to target.
Signature-compatible with pathlib.Path.symlink_to(); force is a
pathapi extension (see docs/divergences.md).
With force=True, an existing entry at this path is removed
first. No filesystem or transport offers an atomic "replace a
symlink" operation, so this is an unlink-then-symlink sequence and
is therefore not atomic: between the two steps the path does not
exist, and a concurrent writer can win the race. It is a
convenience, not a locking primitive.
Only a non-directory entry is removed -- an existing directory at
the link path is left alone and the underlying FileExistsError
(or the backend's equivalent) propagates. Silently deleting a
directory tree is never what force= on a symlink call is asking
for.
Source code in src/pathapi/protocols/links.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
pathapi.protocols.resolve
Making a path absolute and following the links on its way.
__all__ = ['Resolve']
module-attribute
Resolve
Bases: Readlink, Protocol
absolute() and resolve(). _cwd() is the primitive: the directory
a relative path is relative to. resolve() follows links through
readlink() where the class has it and is lexical otherwise.
__primitives__ = (('_cwd', 'absolute'),)
class-attribute
__slots__ = ()
class-attribute
instance-attribute
absolute()
This path when it is absolute, else _cwd() joined with it.
Source code in src/pathapi/protocols/resolve.py
35 36 37 38 39 | |
resolve(strict=False)
The absolute path with .. applied and every link replaced by what
readlink() says, at most 40 of them. strict=True raises
FileNotFoundError for a component that is missing; otherwise the
rest of the names are kept as they are. A link loop raises what
pathlib raises on this interpreter: RuntimeError before 3.13,
OSError(ELOOP) from 3.13 when strict (otherwise the looping link
is kept).
Source code in src/pathapi/protocols/resolve.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | |
pathapi.protocols.transfer
Telling two paths apart, copying and moving.
__all__ = ['Copy', 'Identity', 'Move']
module-attribute
Copy
Bases: Identity, Create, Symlink, Readlink, BinaryOpen, Protocol
Copying a file or a tree to another path, of this backend or of
another. It precedes BinaryOpen in a class's bases because both define
copy() and this one takes the options of a path copy.
__primitives__ = ()
class-attribute
__slots__ = ()
class-attribute
instance-attribute
copy(target, *, overwrite=False, follow_symlinks=True, preserve_metadata=True, recursive=False, ignore_error=None, progress=None, on_error=None)
Copy this file's content to target.
follow_symlinks/preserve_metadata are named to match CPython
3.14's Path.copy() (added after this method); overwrite is our
own extension (3.14 has no equivalent -- it always raises if the
destination exists). preserve_metadata defaults to True here
(unlike 3.14's False): st_mode is propagated, but not
timestamps or xattrs -- full metadata preservation is not implemented.
ignore_error accepts a bool or a callable. True ignores every
error; False and None (the default) fail on the first error. A
callable is invoked as ignore_error(error) and, as it always has
here, is a notification hook: the error is suppressed regardless of
what it returns, so handlers like errors.append (returning None)
keep working. Only errors from child copies during a
recursive=True copy are routed here.
on_error is rm()'s predicate: a callable (error, path) -> bool
(or a bool) asked once per child error, with path the source entry
that failed. True skips the entry and goes on, False raises the
error, and a predicate that raises ends the copy with its own
exception. An error it declined is not offered again by the
directories above. ignore_error and on_error are two spellings of
one policy: passing both raises TypeError before anything is copied.
A child whose listed name is not one safe component for target (a
drive such as C:x or a backslash on a Windows target, which land
outside it; a trailing dot, which Windows stores under another name)
is refused with ValueError and reported through ignore_error or
on_error like any child failure. PathSyncer and
unpack_archive() apply the same rule; an archive listing keeps
such names, as they are ordinary filenames on POSIX.
progress, when given, is called as progress(path, bytes_copied,
total_size) for every chunk written during each file copy (path
is the source Path being streamed -- self for a single-file
copy, or the relevant child during a recursive=True copy).
bytes_copied increases monotonically per file and reaches
total_size (or None if the size couldn't be determined) at the
end of that file. Directories themselves don't get a progress call
(only the files inside them do). With progress=None (the
default), behavior is unchanged -- no per-chunk overhead. Native
backend transfers that bypass the generic streaming copy (e.g.
an SFTP backend's concurrent fan-out) do not invoke progress;
see docs/divergences.md's "Deliberate extensions" section.
Source code in src/pathapi/protocols/transfer.py
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 | |
copy_into(target_dir, *, overwrite=False, follow_symlinks=True, preserve_metadata=True, recursive=False, ignore_error=None, progress=None, on_error=None)
Copy this file or directory into the directory target_dir, under
its own name, and return the new path (pathlib 3.14 parity).
copy(target_dir / self.name, ...) with the same keywords and the
same defaults as copy(): an existing target needs overwrite=True
and a directory source needs recursive=True. ValueError for a
path with no name.
Source code in src/pathapi/protocols/transfer.py
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 | |
Identity
Bases: Stat, Protocol
Whether two paths name the same node, and in which namespace.
_same_filesystem() and _node_key() are overridden by a class whose
instances can front different stores; samefile() compares what
stat() reports.
__primitives__ = ()
class-attribute
__slots__ = ()
class-attribute
instance-attribute
samefile(other_path)
Return whether other_path is the same or not as this file, by comparing (st_dev, st_ino) when the backend's stat() provides them. Raises NotImplementedError otherwise (e.g. our own FileStat doesn't carry st_dev/st_ino) -- LocalPath gets a real implementation from pathlib.Path via MRO instead of this one.
Source code in src/pathapi/protocols/transfer.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
Move
Renaming and moving: rename() is the primitive, and move() falls
back to a copy followed by a removal where it is not available.
__primitives__ = ('rename',)
class-attribute
__slots__ = ()
class-attribute
instance-attribute
move(target, *, overwrite=False)
Move this file or directory to target, falling back to copy+unlink/rm if rename is unsupported.
Nothing is removed until the move is known to be allowed: a missing
source, a file onto a directory, a target that holds the source
(OSError ENOTEMPTY) or lies inside it (EINVAL), and a second
name of the same file that is a link (EINVAL) all raise first. The
same file under another spelling (a case alias, x.move(x)) is
renamed in place. Between two hard links of one file, an existing
target needs overwrite=True, and the source name is then removed.
Source code in src/pathapi/protocols/transfer.py
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 | |
move_into(target_dir, *, overwrite=False)
Move this file or directory into the directory target_dir, under
its own name, and return the new path (pathlib 3.14 parity).
move(target_dir / self.name, overwrite=overwrite): an existing
target needs overwrite=True. ValueError for a path with no name.
Source code in src/pathapi/protocols/transfer.py
575 576 577 578 579 580 581 582 583 | |
rename(target)
Rename this file or directory to the given target.
Source code in src/pathapi/protocols/transfer.py
463 464 465 466 | |
replace(target)
Rename this file or directory to target, replacing what is
there, and return target as a path (pathlib parity).
A file replaces a file and a directory replaces an empty directory;
a directory or a file standing in the way of the other kind raises
as os.replace() does, and a directory that holds anything raises
OSError(ENOTEMPTY) rather than being removed. Built on move(), so
it works between any two paths move() accepts; LocalPath keeps
stdlib's os.replace().
Source code in src/pathapi/protocols/transfer.py
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 | |
pathapi.protocols.checksum
__all__ = ['NativeChecksum']
module-attribute
NativeChecksum
Bases: Protocol
Composable protocol for backends that can compute a file digest server-side, without streaming the file's content through this process.
Optional per-Path-subclass, like every other protocol in this
package (io.BinaryOpen, fs.Stat, fs.Chmod) -- most backends will
never implement it, and the base Path ABC does not require it.
sync.PathSyncer (and any other caller) is expected to try this
first and fall back to a streaming checksum (checksum.md5/
sha256) on NotImplementedError.
Two methods, two different jobs: supported_checksums() is an
advisory capability query (never raises, so a caller can pick a
shared algorithm across two paths -- e.g.
source.supported_checksums() & target.supported_checksums() -- before
calling anything expensive); checksum() is the authoritative
per-call contract and still MUST raise NotImplementedError on its own
for any algorithm it can't actually produce, even if a caller never
consulted supported_checksums() first (belt-and-suspenders -- the
checksum method's own contract, not just the advertisement, is what a
correctness-sensitive caller can rely on).
__primitives__ = ('checksum',)
class-attribute
__slots__ = ()
class-attribute
instance-attribute
checksum(algorithm='md5')
Return a hex-digest checksum of this file's content, computed by
the backend itself (e.g. an SFTP server implementing the filexfer draft's
check-file-handle extension -- OpenSSH does not --, a WebDAV getetag, ...) rather than by streaming the
content through open("rb").
algorithm names a hashlib-style digest (at least "md5" must
be accepted, matching PathSyncer's current default). A backend
that cannot produce the requested algorithm -- whether because it
supports no native hashing at all, or only a different algorithm --
MUST raise NotImplementedError rather than silently returning a
digest under a different algorithm or a value that isn't a true
content hash. This is a hard requirement, not a style preference:
comparing two checksums computed under different algorithms (or
comparing a real hash to something hash-shaped but not actually a
content digest, e.g. S3's ETag for a multipart upload) can silently
produce a false "in sync" verdict. Callers must never catch
anything broader than NotImplementedError here.
supported_checksums() not listing algorithm is advisory, not a
substitute for this method enforcing its own contract.
Source code in src/pathapi/protocols/checksum.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | |
supported_checksums()
Advisory set of algorithm names this path can currently
produce a native digest for (e.g. frozenset({"md5"})), so a
caller can pick a shared algorithm across two paths -- or fall back
to streaming -- without probing via trial-and-NotImplementedError
per candidate. Never raises. The default (no override, matching the
base Path/Pathname ABC not implementing this protocol at all)
is an empty frozenset() -- "no native support" -- which is also
the correct answer for a backend whose native-hashing capability
can vary at runtime (e.g. per-connection extension negotiation) and
currently has none available.
This can be a live/dynamic check (e.g. reflecting whether the current server connection actually advertised a given SFTP extension), not necessarily a hardcoded class-level constant -- implementations should recompute it if capability can change within the object's lifetime, and are free to cache it if not.
Source code in src/pathapi/protocols/checksum.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | |