Skip to content

URI API

Uri (pure URI paths), UriPath (URI paths with I/O and scheme dispatch), Source and Query. The built-in schemes are documented under Schemes.

pathlib_next.uri

UriLike = 'str | Uri | os.PathLike' module-attribute

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
157
158
159
160
161
162
163
164
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
191
192
193
194
195
196
197
198
199
200
201
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, which
                # joins like a relative PurePath (see _RelativeLocalPath).
                uri = _RelativeLocalPath(_uriencode(uri.as_posix(), safe="/"))
            _uris.append(uri)
        elif isinstance(uri, (_pathlib.PurePath, Pathname)):
            _uris.append(_path_reference(uri.as_posix()))
        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(_path_reference(posix))
    self._raw_uris = _uris

fragment property

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).

path property

query property

The query exactly as received: still percent-encoded, so an escaped &, = or + inside a value survives the round trip. Use Query.decode()/to_dict() for decoded pairs.

segments property

source property

stem property

suffix property

as_posix()

Source code in src/pathlib_next/uri/__init__.py
816
817
818
819
820
821
822
823
824
825
826
827
def as_posix(self):
    source = self.source
    host = None
    posix = self.path
    if source.host:
        host = source.host
        user, password = source.parsed_userinfo()
        if user:
            posix = f"{user}@{host}:{posix}"
        else:
            posix = f"{host}:{posix}"
    return posix

as_uri(sanitize=False)

Source code in src/pathlib_next/uri/__init__.py
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
def as_uri(self, /, sanitize=False):
    if self._uri is None or sanitize:
        path = self.path
        if path.startswith("//") and not self._has_authority():
            # Without an authority a "//" path would render as one
            # ("file:////srv/x" is host "" and path "//srv/x"), which
            # raised ValueError from str()/repr()/hash(). "/." keeps it
            # a path and parses back to the same one.
            path = "/." + path
        uri = self._format_parsed_parts(
            self.source, path, self.query, self.fragment, sanitize=sanitize
        )
        if not sanitize:
            self._uri = uri
        return uri
    else:
        return self._uri

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
529
530
531
532
533
534
535
536
537
538
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: it starts with "/", with or without a source (as PurePosixPath("/a") is absolute).

Source code in src/pathlib_next/uri/__init__.py
705
706
707
708
def is_absolute(self):
    """True if the path is absolute: it starts with "/", with or without
    a source (as `PurePosixPath("/a")` is absolute)."""
    return self.path.startswith("/")

is_local()

Return True if the URI points to a local resource.

Source code in src/pathlib_next/uri/__init__.py
765
766
767
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
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
def is_relative_to(self, other: UriLike):
    """Return True if the path is relative to another path or False."""
    # Uri(other), NOT Uri(self, _ROOT, other): anchoring a str `other`
    # at self's root turned `Uri("a/b").is_relative_to("a")` into a
    # comparison against "/a" and answered False, disagreeing with the
    # object form of the same call. `relative_to()` below already
    # parsed a str standalone; this matches it and CPython.
    # The same-authority case still works: a standalone parse leaves
    # `other.source` empty, which the guard below treats as compatible
    # with any `self.source`, and the segment prefix compare is
    # unaffected -- `Uri("http://h/a/b").is_relative_to("/a")` is True.
    other = other if isinstance(other, Uri) else Uri(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 = other._prefix_segments()
    _self = self._prefix_segments()
    if _self[:1] == [""] and _other[:1] != [""]:
        # An absolute path is never relative to a relative one (the
        # empty relative path included), as in pathlib.
        return False
    return _self[: len(_other)] == _other

joinpath(*args)

Combine this URI with segments; a str is a decoded path.

Source code in src/pathlib_next/uri/__init__.py
796
797
798
799
800
801
802
803
804
805
806
def joinpath(self, *args):
    """Combine this URI with segments; a `str` is a decoded path."""
    result = self
    for arg in args:
        arg = result._join_arg(arg)
        result = (
            result._join_decoded(arg)
            if isinstance(arg, str)
            else type(result)(result, arg)
        )
    return result

relative_to(other, *, walk_up=False)

Source code in src/pathlib_next/uri/__init__.py
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
def relative_to(self, other: UriLike, *, walk_up=False):
    other = other if isinstance(other, Uri) else Uri(other)
    # NOTE: no upfront `if not self.is_relative_to(other): raise` here --
    # that used to short-circuit before the walk_up loop below ever ran,
    # making walk_up=True dead code. The step==0 iteration of the loop
    # (path=other) reproduces the exact same non-walk_up error.
    for step, path in enumerate([other] + list(other.parents)):
        if self.is_relative_to(path):
            break
        elif not walk_up:
            raise ValueError(
                f"{str(self)!r} is not in the subpath of {str(other)!r}"
            )
        elif path.name == "..":
            raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
    else:
        raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
    # _segments_of, not raw .segments: the latter gives the URI root
    # ("/") a spurious 2-tuple ("", "") instead of ("",), which used to
    # make relative_to(<root>) drop the child's only real segment
    # (found via property testing, polish_perf/06).
    self_segs = self._prefix_segments()
    path_segs = path._prefix_segments()
    parts = [".."] * step + self_segs[len(path_segs) :]
    return self._from_parsed_parts(
        _NOSOURCE, "/".join(parts), self.query, self.fragment
    )

with_fragment(fragment)

Return a new URI with the fragment replaced.

Source code in src/pathlib_next/uri/__init__.py
640
641
642
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
621
622
623
624
625
626
627
628
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.

A str is taken in the percent-encoded form .query returns and is sent as given; a mapping or a sequence of pairs is encoded by Query.

Source code in src/pathlib_next/uri/__init__.py
630
631
632
633
634
635
636
637
638
def with_query(self, query: str):
    """Return a new URI with the query replaced.

    A `str` is taken in the percent-encoded form `.query` returns and is
    sent as given; a mapping or a sequence of pairs is encoded by
    `Query`."""
    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
615
616
617
618
619
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
611
612
613
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
157
158
159
160
161
162
163
164
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
191
192
193
194
195
196
197
198
199
200
201
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, which
                # joins like a relative PurePath (see _RelativeLocalPath).
                uri = _RelativeLocalPath(_uriencode(uri.as_posix(), safe="/"))
            _uris.append(uri)
        elif isinstance(uri, (_pathlib.PurePath, Pathname)):
            _uris.append(_path_reference(uri.as_posix()))
        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(_path_reference(posix))
    self._raw_uris = _uris

backend property

The connection or session state backend instance.

iterdir()

Source code in src/pathlib_next/uri/__init__.py
1216
1217
1218
def iterdir(self) -> "_ty.Iterator[Self]":
    for name, stat in self._scandir():
        yield self._make_child_relpath(name, stat_hint=stat)

joinpath(*args)

Combine this path with segments, choosing the result's class from its scheme as / does: joining an absolute local path gives a FileUri, not this class carrying a file: URI.

Each str argument is an already-decoded path (see Uri._join_arg); pass a Uri for a scheme-aware join.

Source code in src/pathlib_next/uri/__init__.py
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
def joinpath(self, *args: str | Uri | os.PathLike) -> "UriPath":
    """Combine this path with segments, choosing the result's class from
    its scheme as `/` does: joining an absolute local path gives a
    `FileUri`, not this class carrying a `file:` URI.

    Each `str` argument is an already-decoded path (see
    `Uri._join_arg`); pass a `Uri` for a scheme-aware join."""
    result = self
    for arg in args:
        arg = result._join_arg(arg)
        result = (
            result._join_decoded(arg)
            if isinstance(arg, str)
            else type(result)(result, arg, findclass=True)
        )
    return result

with_backend(backend)

Return a new path instance sharing the same backend state.

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

with_source(source)

Source code in src/pathlib_next/uri/__init__.py
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
def with_source(self, source: Source):
    cls = type(self)
    if not source or not source.scheme:
        # Nothing to dispatch on (`source.scheme + ":"` raised TypeError
        # for a host-only Source): a plain UriPath.
        inst = Uri.__new__(UriPath)
    elif source.scheme not in cls._schemes():
        inst = cls.__new__(cls, source.scheme + ":", findclass=True)
    else:
        self._check_inherited_backend()
        same = _same_authority(source, self.source)
        inst = cls.__new__(cls, backend=self._backend if same else None)
    inst._init(source, self.path, self.query, self.fragment)
    return inst

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.

host instance-attribute

port instance-attribute

scheme instance-attribute

userinfo instance-attribute

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
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
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,
    )

from_str(source, strict=True) classmethod

Source code in src/pathlib_next/uri/source.py
310
311
312
313
314
315
316
317
318
319
320
321
@classmethod
def from_str(cls, source: str, strict=True):
    uri = _uritools.urisplit(source)
    if strict and (uri.path or uri.fragment or uri.query):
        raise ValueError(source)
    scheme = uri.scheme.lower() if uri.scheme is not None else None
    userinfo, host, port = _split_authority(uri.authority)
    if userinfo is not None:
        userinfo = _uritools.uridecode(userinfo, errors=_ERRORS)
    if host is not None:
        host = _decode_host(host)
    return cls(scheme, userinfo, host, port)

get_scheme_cls(schemesmap=None)

The UriPath subclass registered for this scheme, or UriPath itself. An explicit schemesmap is the complete set of classes to choose from: a scheme missing from it gives UriPath, and neither plugins nor the global registry are consulted.

Source code in src/pathlib_next/uri/source.py
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
def get_scheme_cls(self, schemesmap: _ty.Mapping[str, type["UriPath"]] = None):
    """The `UriPath` subclass registered for this scheme, or `UriPath`
    itself. An explicit `schemesmap` is the complete set of classes to
    choose from: a scheme missing from it gives `UriPath`, and neither
    plugins nor the global registry are consulted."""
    from . import UriPath

    if self.scheme:
        if schemesmap is not None:
            return schemesmap.get(self.scheme, None) or UriPath
        schemesmap = UriPath._schemesmap()
        _cls = schemesmap.get(self.scheme, None)
        if _cls is None:
            # The map is rebuilt whenever a UriPath subclass is defined
            # (UriPath.__init_subclass__), so a miss here is a scheme no
            # imported class registers; `_load_entry_point` caches that
            # negative answer instead of rescanning every installed
            # distribution on each construction.
            if UriPath._load_entry_point(
                self.scheme
            ) or UriPath._load_builtin_scheme(self.scheme):
                schemesmap = UriPath._schemesmap(reload=True)
                _cls = schemesmap.get(self.scheme, None)
        return _cls if _cls else UriPath
    return UriPath

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. 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
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
@_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. 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
    # Imported here, not at module top: netimps calls platform.node() at
    # import (a WMI query on Windows), a cost every `import pathlib_next`
    # paid although only this method needs it.
    import netimps as _netimps

    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)

keys()

Source code in src/pathlib_next/uri/source.py
323
324
def keys(self):
    return self._asdict().keys()

parsed_userinfo()

Source code in src/pathlib_next/uri/source.py
331
332
333
334
335
336
def parsed_userinfo(self):
    parts = []
    if self.userinfo:
        parts = self.userinfo.split(":", maxsplit=1)
    parts = parts + ["", ""]
    return parts[0], parts[1]

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.

The string is always the percent-encoded form: a str argument is taken as already encoded (it is what Uri.query holds, as received), a mapping or pair sequence is encoded here, and decode()/to_dict() decode each name and value exactly once.

ENCODING = 'utf-8' class-attribute instance-attribute

SEPARATOR = '&' class-attribute instance-attribute

decode(query)

Source code in src/pathlib_next/uri/query.py
94
95
96
97
def decode(query) -> list[tuple[str, str | None]]:
    return _uritools.SplitResultString("", "", "", str(query), "").getquerylist(
        query._separator, query._encoding
    )

to_dict(query, *, single=False)

Source code in src/pathlib_next/uri/query.py
102
103
104
105
106
107
108
109
def to_dict(query, *, single=False):
    query_: dict[str, list[str | None]] = {}
    for k, v in query.decode():
        if single:
            query_[k] = v
        else:
            query_.setdefault(k, []).append(v)
    return query_