Skip to content

URI & Schemes API

pathlib_next.uri

Uri(*uris, **options)

Bases: Pathname

A pure (no I/O) RFC 3986 URI, lazily parsed into source (scheme/ userinfo/host/port), path, query, and fragment on first access. Join semantics (multiple constructor args, or /) are pathlib- joinpath-like, not RFC 3986 reference resolution -- see _load_parts's docstring and docs/divergences.md.

Source code in src/pathlib_next/uri/__init__.py
 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
def __init__(self, *uris: UriLike, **options):
    if self._raw_uris or self._initiated:
        return
    _uris: list[str | Uri] = []
    for uri in uris:
        if not uri:
            uri = ""
        if isinstance(uri, Uri):
            _uris.append(uri)
        elif isinstance(uri, (_pathlib.Path, Path)):
            try:
                uri = uri.as_uri()
            except ValueError:
                # as_uri() raises ValueError for a relative path.
                uri = f"file:{_uriencode(uri.as_posix(), safe='/')}"
            _uris.append(uri)
        elif isinstance(uri, (_pathlib.PurePath, Pathname)):
            _uris.append(f"{_uriencode(uri.as_posix(), safe='/')}")
        elif hasattr(uri, "as_uri"):
            path = uri.as_uri
            if callable(path):
                path = path()
            _uris.append(path)
        elif isinstance(uri, str):
            _uris.append(uri)
        elif isinstance(uri, bytes):
            _uris.append(uri.decode())
        else:
            path = None
            try:
                path = os.fspath(uri)
            except (TypeError, NotImplementedError):
                pass
            if not isinstance(path, str):
                raise TypeError(
                    "argument should be a str or an os.PathLike "
                    "object where __fspath__ returns a str, "
                    f"not {type(path).__name__!r}"
                )
            # Only __fspath__ is guaranteed here -- posix-normalize the
            # string itself rather than assuming an as_posix() method.
            posix = _pathlib.PurePath(path).as_posix()
            _uris.append(f"{_uriencode(posix, safe='/')}")
    self._raw_uris = _uris

normalized_path property

Return the normalized path using posixpath rules.

parent property

The logical parent of the path.

parts property

The tuple of URI components: (source, path, query, fragment).

__str__()

Return the string representation of the path. Deliberately sanitized (password dropped from userinfo) since str() is what logging/printing reach for -- this does NOT round-trip a credentialed URI. Use as_uri(sanitize=False) for the full URI including credentials.

Source code in src/pathlib_next/uri/__init__.py
260
261
262
263
264
265
266
def __str__(self):
    """Return the string representation of the path. Deliberately
    sanitized (password dropped from userinfo) since `str()` is what
    logging/printing reach for -- this does NOT round-trip a
    credentialed URI. Use `as_uri(sanitize=False)` for the full URI
    including credentials."""
    return self.as_uri(sanitize=True)

host_fspath()

Return .path for any scheme whose path component is a filesystem path on the URI's own host (see _host_filesystem_path), for building a command line that runs on that host (e.g. via a remote executor). Unlike __fspath__, this never falls back to treating the path as local -- it raises NotImplementedError for schemes with no host-filesystem meaning (http:, s3:, ...).

Source code in src/pathlib_next/uri/__init__.py
282
283
284
285
286
287
288
289
290
291
def host_fspath(self) -> str:
    """Return `.path` for any scheme whose path component is a
    filesystem path on the URI's own host (see `_host_filesystem_path`),
    for building a command line that runs *on that host* (e.g. via a
    remote executor). Unlike `__fspath__`, this never falls back to
    treating the path as local -- it raises `NotImplementedError` for
    schemes with no host-filesystem meaning (`http:`, `s3:`, ...)."""
    if self._host_filesystem_path:
        return self.path
    raise NotImplementedError(f"host_fspath for {self.source.scheme}")

is_absolute()

True if the path is absolute.

Source code in src/pathlib_next/uri/__init__.py
429
430
431
def is_absolute(self):
    """True if the path is absolute."""
    return bool(self.source) and self.path.startswith("/")

is_local()

Return True if the URI points to a local resource.

Source code in src/pathlib_next/uri/__init__.py
475
476
477
def is_local(self):
    """Return True if the URI points to a local resource."""
    return self.source.is_local()

is_relative_to(other)

Return True if the path is relative to another path or False.

Source code in src/pathlib_next/uri/__init__.py
433
434
435
436
437
438
439
440
441
442
443
444
445
def is_relative_to(self, other: UriLike):
    """Return True if the path is relative to another path or False."""
    other = other if isinstance(other, Uri) else Uri(self, _ROOT, other)
    if not (
        (other.source == self.source)
        or not (bool(self.source) and bool(other.source))
    ):
        return False
    # Segment-wise prefix comparison: a naive startswith() on the raw
    # strings would report "/foo/bar2" as relative to "/foo/bar".
    _other = _segments_of(other.normalized_path)
    _self = _segments_of(self.normalized_path)
    return _self[: len(_other)] == _other

with_fragment(fragment)

Return a new URI with the fragment replaced.

Source code in src/pathlib_next/uri/__init__.py
379
380
381
def with_fragment(self, fragment: str):
    """Return a new URI with the fragment replaced."""
    return self._from_parsed_parts(self.source, self.path, self.query, fragment)

with_path(path)

Return a new URI with the path replaced.

Source code in src/pathlib_next/uri/__init__.py
364
365
366
367
368
369
370
371
def with_path(self, path: str | Pathname):
    """Return a new URI with the path replaced."""
    return self._from_parsed_parts(
        self.source,
        path.as_posix() if isinstance(path, Pathname) else path,
        self.query,
        self.fragment,
    )

with_query(query)

Return a new URI with the query replaced.

Source code in src/pathlib_next/uri/__init__.py
373
374
375
376
377
def with_query(self, query: str):
    """Return a new URI with the query replaced."""
    if not isinstance(query, Query):
        query = Query(query)
    return self._from_parsed_parts(self.source, self.path, query, self.fragment)

with_segments(*segments)

Return a new URI with the path segments replaced.

Source code in src/pathlib_next/uri/__init__.py
358
359
360
361
362
def with_segments(self, *segments: str):
    """Return a new URI with the path segments replaced."""
    if not segments:
        return self.with_path("")
    return self.with_path("/".join(segments))

with_source(source)

Return a new URI with the source replaced.

Source code in src/pathlib_next/uri/__init__.py
354
355
356
def with_source(self, source: Source):
    """Return a new URI with the source replaced."""
    return self._from_parsed_parts(source, self.path, self.query, self.fragment)

UriPath(*uris, **options)

Bases: Uri, Path

Uri + Path (I/O) + scheme dispatch. UriPath(...) constructs the concrete subclass registered for the URI's scheme (via __SCHEMES) -- e.g. UriPath("http://...") returns an HttpPath. Subclass this and set __SCHEMES to add a new scheme (Track B of extending this library; see docs/guides/extending.md); implement the I/O surface (_listdir or _scandir, stat, _open, ...) documented in docs/guides/extending.md. Prefer overriding _scandir() over _listdir() when the listing call already returns type/size/mtime metadata (PROPFIND, MLSD, listdir_attr, an S3 list page, ...) -- walk()/glob() then answer is_dir() on the results for free, without a stat request per entry.

Source code in src/pathlib_next/uri/__init__.py
 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
def __init__(self, *uris: UriLike, **options):
    if self._raw_uris or self._initiated:
        return
    _uris: list[str | Uri] = []
    for uri in uris:
        if not uri:
            uri = ""
        if isinstance(uri, Uri):
            _uris.append(uri)
        elif isinstance(uri, (_pathlib.Path, Path)):
            try:
                uri = uri.as_uri()
            except ValueError:
                # as_uri() raises ValueError for a relative path.
                uri = f"file:{_uriencode(uri.as_posix(), safe='/')}"
            _uris.append(uri)
        elif isinstance(uri, (_pathlib.PurePath, Pathname)):
            _uris.append(f"{_uriencode(uri.as_posix(), safe='/')}")
        elif hasattr(uri, "as_uri"):
            path = uri.as_uri
            if callable(path):
                path = path()
            _uris.append(path)
        elif isinstance(uri, str):
            _uris.append(uri)
        elif isinstance(uri, bytes):
            _uris.append(uri.decode())
        else:
            path = None
            try:
                path = os.fspath(uri)
            except (TypeError, NotImplementedError):
                pass
            if not isinstance(path, str):
                raise TypeError(
                    "argument should be a str or an os.PathLike "
                    "object where __fspath__ returns a str, "
                    f"not {type(path).__name__!r}"
                )
            # Only __fspath__ is guaranteed here -- posix-normalize the
            # string itself rather than assuming an as_posix() method.
            posix = _pathlib.PurePath(path).as_posix()
            _uris.append(f"{_uriencode(posix, safe='/')}")
    self._raw_uris = _uris

backend property

The connection or session state backend instance.

with_backend(backend)

Return a new path instance sharing the same backend state.

Source code in src/pathlib_next/uri/__init__.py
664
665
666
def with_backend(self, backend):
    """Return a new path instance sharing the same backend state."""
    return self._from_parsed_parts(*self.parts, backend=backend)

pathlib_next.uri.source

Source

Bases: NamedTuple

A URI's scheme/userinfo/host/port -- everything before the path. Falsy (bool(source) is False) when every field is empty/None.

__str__()

Deliberately sanitized (password dropped from userinfo), same rationale as Uri.__str__: this is what logging/printing reach for, and a Source on a failing call stack must not leak a credential. Does NOT round-trip a credentialed source -- use as_str(sanitize=False) for the unredacted form.

Source code in src/pathlib_next/uri/source.py
230
231
232
233
234
235
236
237
def __str__(self) -> str:
    """Deliberately sanitized (password dropped from `userinfo`), same
    rationale as `Uri.__str__`: this is what logging/printing reach
    for, and a `Source` on a failing call stack must not leak a
    credential. Does NOT round-trip a credentialed source -- use
    `as_str(sanitize=False)` for the unredacted form.
    """
    return self.as_str(sanitize=True)

as_str(sanitize=True)

Compose this Source back into an authority string (scheme://userinfo@host:port). sanitize=True (the default, matching __str__) drops the password from userinfo; pass sanitize=False for the full, credentialed round trip -- the same escape hatch Uri.as_uri(sanitize=False) provides one layer up. Mirrors Uri.as_uri()'s name/kwarg exactly so both classes are used the same way.

Source code in src/pathlib_next/uri/source.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
def as_str(self, /, sanitize=True) -> str:
    """Compose this `Source` back into an authority string
    (`scheme://userinfo@host:port`). `sanitize=True` (the default,
    matching `__str__`) drops the password from `userinfo`; pass
    `sanitize=False` for the full, credentialed round trip -- the
    same escape hatch `Uri.as_uri(sanitize=False)` provides one layer
    up. Mirrors `Uri.as_uri()`'s name/kwarg exactly so both classes
    are used the same way.
    """
    return _uritools.uricompose(
        scheme=self.scheme,
        userinfo=self._redacted_userinfo() if sanitize else self.userinfo,
        host=self.host,
        port=self.port,
    )

is_local() cached

Whether host resolves to this machine.

Caches per unique Source (Source is an immutable value type), since this does a DNS/hosts-file lookup -- never call it on a hot path uncached.

The hostname->address step uses netimps.resolve() (default backend chain: dnspython, then the OS resolver via getaddrinfo() -- hosts file, NSS, DNS, OS cache -- then nslookup as a last resort), trying both "a"/"aaaa" record types and treating host as local if ANY resolved address is. netimps.resolve() gained OS-resolver-chain support in 0.2.0 -- before that it was dnspython-only, which is why this method originally kept socket.gethostbyname() for this step (see .agents/findings/processed/2026-07-29_netimps_adoption_survey.md and the companion finding filed against netimps itself). The "is this address MINE" comparison uses netimps.is_local_address(), which enumerates real network interfaces (netimps.get_interfaces()) instead of the weaker socket.getaddrinfo(socket.gethostname(), None) this project used originally -- that approach missed addresses not tied to the resolvable hostname (VMs, containers, VPN interfaces, additional NICs on a multi-homed host).

host as a bare IP-literal str (e.g. a directly-constructed Source(..., host="::1", ...), bypassing _decode_host()'s usual bracket-literal parsing) is handled by netimps.try_parse() without going through resolution at all.

Source code in src/pathlib_next/uri/source.py
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
@_functools.lru_cache(maxsize=256)
def is_local(self):
    """Whether `host` resolves to this machine.

    Caches per unique Source (Source is an immutable value type), since
    this does a DNS/hosts-file lookup -- never call it on a hot path
    uncached.

    The hostname->address step uses `netimps.resolve()` (default
    backend chain: dnspython, then the OS resolver via
    `getaddrinfo()` -- hosts file, NSS, DNS, OS cache -- then
    `nslookup` as a last resort), trying both `"a"`/`"aaaa"` record
    types and treating `host` as local if ANY resolved address is.
    `netimps.resolve()` gained OS-resolver-chain support in 0.2.0 --
    before that it was dnspython-only, which is why this method
    originally kept `socket.gethostbyname()` for this step (see
    `.agents/findings/processed/2026-07-29_netimps_adoption_survey.md`
    and the companion finding filed against `netimps` itself). The
    "is this address MINE" comparison uses `netimps.is_local_address()`,
    which enumerates real network interfaces
    (`netimps.get_interfaces()`) instead of the weaker
    `socket.getaddrinfo(socket.gethostname(), None)` this project used
    originally -- that approach missed addresses not tied to the
    resolvable hostname (VMs, containers, VPN interfaces, additional
    NICs on a multi-homed host).

    `host` as a bare IP-literal `str` (e.g. a directly-constructed
    `Source(..., host="::1", ...)`, bypassing `_decode_host()`'s usual
    bracket-literal parsing) is handled by `netimps.try_parse()`
    without going through resolution at all.
    """
    host = self.host
    if not host or host == "localhost":
        return True
    if not isinstance(host, str):
        return _netimps.is_local_address(host)
    literal = _netimps.try_parse(host)
    if literal is not None:
        return _netimps.is_local_address(literal)
    addresses = _netimps.resolve(host, "a") + _netimps.resolve(host, "aaaa")
    return any(_netimps.is_local_address(address) for address in addresses)

pathlib_next.uri.query

Query

Bases: str

A URI query string (str subclass) that can also be built from a dict/list of pairs and decoded back with to_dict()/iteration.