Skip to content

Memory Path API

pathapi.mempath

MemBytesIO(dest, *, append=False)

Bases: BytesIO

A BytesIO that writes its buffer back into the backing bytearray (dest) on flush() and close, so MemPath files persist across open() calls. With append=True every write lands at the end, like O_APPEND, whatever the current position, and only the bytes this handle wrote are added to dest, so two appenders both land.

Source code in src/pathapi/mempath.py
65
66
67
68
69
70
71
72
73
def __init__(self, dest: bytearray, *, append: bool = False) -> None:
    self._bytes = dest
    self._append = append
    self._published = 0
    super().__init__()
    if append:
        snapshot = bytes(dest)
        super().write(snapshot)
        self._published = len(snapshot)

close()

Source code in src/pathapi/mempath.py
106
107
108
109
def close(self) -> None:
    if not self.closed:
        self._publish()
    return super().close()

flush()

Source code in src/pathapi/mempath.py
101
102
103
104
def flush(self) -> None:
    # A real file's flush makes the written bytes visible to readers.
    super().flush()
    self._publish()

write(data)

Source code in src/pathapi/mempath.py
91
92
93
94
def write(self, data) -> int:
    if self._append and not self.closed:
        self.seek(0, io.SEEK_END)
    return super().write(data)

writelines(lines)

Source code in src/pathapi/mempath.py
96
97
98
99
def writelines(self, lines) -> None:
    # BytesIO.writelines bypasses an overridden write().
    for line in lines:
        self.write(line)

MemFile(*args)

Bases: bytearray

A file's content in a MemPathBackend, carrying its modification time. A plain bytearray placed in a backend by hand still works; it reports st_mtime 0.

Source code in src/pathapi/mempath.py
31
32
33
34
def __init__(self, *args) -> None:
    super().__init__(*args)
    self.mtime = 0.0
    _touch(self)

mtime = 0.0 instance-attribute

MemPath(*segments, backend=None)

Bases: Path

In-memory Path implementation over nested dicts (see MemPathBackend) -- a lightweight virtual filesystem for mocks, tests, or transient storage, and the reference exemplar for Track A of extending this library (subclassing Path directly; see docs/guides/extending.md).

Source code in src/pathapi/mempath.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def __init__(
    self, *segments: str | Pathname | Path, backend: MemPathBackend = None
):
    # Joined and normalized like `PurePosixPath`: empty and "." segments collapse and
    # an absolute argument restarts the join (`MemPath("/root") / "/etc"` is "/etc").
    path = ""
    _backend = None
    for segment in segments:
        if isinstance(segment, MemPath):
            text = segment.as_posix()
            # The first MemPath argument names the filesystem, as the
            # left operand does for `/`; a later one (often a pattern or
            # a name built without a backend) only contributes its text.
            if _backend is None:
                _backend = segment.backend
        elif isinstance(segment, Path):
            raise NotImplementedError()
        elif isinstance(segment, Pathname):
            text = "/".join(segment.segments)
        elif isinstance(segment, str):
            text = segment
        else:
            raise TypeError(
                "argument should be a str or a Pathname, "
                f"not {type(segment).__name__!r}"
            )
        if text.startswith("/") or not path:
            path = text
        elif text:
            path = f"{path}/{text}"
    self._segments = self._parse(path)
    # `is not None`, not truthiness: a fresh root's backend is an empty dict, which
    # is falsy, and a join off it would get a disconnected new backend.
    if _backend is not None and backend is None:
        backend = _backend
    self._backend = backend if backend is not None else MemPathBackend()
    self._normalized = None

backend property

normalized property

The names of this path with .. applied to the text and clamped at the root; [""] for the root. _node_key() reads it.

parent property

parts property

segments property

as_uri()

Source code in src/pathapi/mempath.py
284
285
def as_uri(self):
    return f"mempath:{_urlquote(self.as_posix())}"

iterdir()

Source code in src/pathapi/mempath.py
392
393
394
395
396
397
398
399
400
def iterdir(self):
    parent, name = self._parent_container()
    content = parent.get(name) if name else parent
    if content is None:
        raise _os_error(FileNotFoundError, _errno.ENOENT, self)
    if not isinstance(content, dict):
        raise _os_error(NotADirectoryError, _errno.ENOTDIR, self)
    for c in list(content.keys()):
        yield self.with_segments(*self.segments, c)

relative_to(other, *, walk_up=False)

self below other, as a relative path on the same backend.

other is a MemPath of the same backend or a str. Names are compared as written, with no .. applied. ValueError when self is not below other, when the two differ in being rooted, or when the backends differ. With walk_up, a path beside other gets a .. per level climbed; a .. in other cannot be walked.

Source code in src/pathapi/mempath.py
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
def relative_to(self, other, *, walk_up: bool = False):
    """`self` below `other`, as a relative path on the same backend.

    `other` is a `MemPath` of the same backend or a `str`. Names are
    compared as written, with no `..` applied. `ValueError` when `self`
    is not below `other`, when the two differ in being rooted, or when
    the backends differ. With `walk_up`, a path beside `other` gets a
    `..` per level climbed; a `..` in `other` cannot be walked."""
    if isinstance(other, MemPath):
        if other.backend is not self.backend:
            raise ValueError(
                f"{str(self)!r} and {str(other)!r} are on different backends"
            )
    else:
        other = self.with_segments(other)
    rooted = self.segments[:1] == ("",)
    other_rooted = other.segments[:1] == ("",)
    names = [name for name in self.segments if name]
    other_names = [name for name in other.segments if name]
    for step in range(len(other_names) + 1):
        keep = len(other_names) - step
        if rooted == other_rooted and names[:keep] == other_names[:keep]:
            return self.with_segments(*[".."] * step, *names[keep:])
        if not walk_up:
            raise ValueError(
                f"{str(self)!r} is not in the subpath of {str(other)!r}"
            )
        if keep and other_names[keep - 1] == "..":
            raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
    raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")

rmdir()

Source code in src/pathapi/mempath.py
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
def rmdir(self):
    parent, name = self._parent_container()
    if not name:
        # The root exists, so it is not "missing", and a tree cannot lose it.
        raise OSError(
            _errno.EBUSY, "The root of a MemPath tree cannot be removed", str(self)
        )
    content = parent.get(name)
    if content is None:
        raise _os_error(FileNotFoundError, _errno.ENOENT, self)
    elif not isinstance(content, dict):
        raise _os_error(NotADirectoryError, _errno.ENOTDIR, self)
    elif len(content) != 0:
        # pathlib raises OSError(ENOTEMPTY); FileExistsError (EEXIST)
        # carried no errno and matched no caller's ENOTEMPTY check.
        raise OSError(_errno.ENOTEMPTY, "Directory not empty", str(self))
    parent.pop(name)

stat(*, follow_symlinks=True)

Source code in src/pathapi/mempath.py
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
def stat(self, *, follow_symlinks=True):
    parent, name = self._parent_container()
    if not name:
        return FileStat(is_dir=True)

    if name not in parent:
        raise _os_error(FileNotFoundError, _errno.ENOENT, self)

    content = parent[name]
    is_dir = isinstance(content, dict)
    # st_size was never set for files (always defaulted to 0), which
    # silently broke any size-based checksum (e.g. PathSyncer's default
    # usage pattern).
    return FileStat(
        is_dir=is_dir,
        st_size=0 if is_dir else len(content),
        st_mtime=getattr(content, "mtime", 0),
    )
Source code in src/pathapi/mempath.py
347
348
349
350
351
352
353
354
355
356
357
358
359
def unlink(self, missing_ok=False):
    parent, name = self._parent_container()
    if not name:
        # The root exists and is a directory: missing_ok does not apply.
        raise _os_error(IsADirectoryError, _errno.EISDIR, self)
    content = parent.get(name)
    if content is None:
        if missing_ok:
            return
        raise _os_error(FileNotFoundError, _errno.ENOENT, self)
    elif isinstance(content, dict):
        raise _os_error(IsADirectoryError, _errno.EISDIR, self)
    parent.pop(name)

with_segments(*segments)

Source code in src/pathapi/mempath.py
277
278
279
280
281
282
def with_segments(self, *segments: str):
    if all(isinstance(segment, str) for segment in segments):
        # The `Pathname` protocol's spelling: segments joined with "/",
        # a leading "" marking the root (`("", "a")` is "/a").
        segments = ("/".join(segments),)
    return type(self)(*segments, backend=self.backend)

MemPathBackend

Bases: dict

Nested-dict storage backing one or more MemPath trees. A dict value is a directory; a bytearray value is a file's content. Share one instance across MemPaths (via backend=) to give them the same virtual filesystem.