Source code for nix_daemon_protocol.store_path

"""A Nix store path: one class, holding the base name.

**Nix holds the base name and nothing else.**
`src/libstore/include/nix/store/path.hh`::

    class StorePath
    {
        std::string baseName;

The store directory is not part of the value. It belongs to the store, at
`StoreDirConfig::storeDir`, and `printStorePath` is what puts it in front.

This module used to hold a second, different `StorePath`: a `WireScalar`, so
a `str`, holding the whole path -- while `pynixd.store_path` held the one
below. The two were not `==`, did not hash alike, and a dict keyed by one
never answered the other. Issue #3 holds the measurement. There is one class
now, and `pynixd.store_path` re-exports it.

**It is not a `str`.** Measured before the change: no call site depended on
the `str`-ness. Every str method called on a store path in the repository was
in `tests/functional`, and all of them were on `stdout.strip()` from a
subprocess. A `str` subclass also makes the wrong thing easy -- `path[:10]`
slices text that means nothing -- and it hides the question this class exists
to answer, which is which form a path is in.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any, Self

from pydantic_core import core_schema

from .store_dir import store_prefix


[docs] class StorePath: """A Nix store path, held as its base name. The store directory is stripped on construction and put back by `__str__`, so a value never carries it twice and never carries it in one place and not another. """ __slots__ = ("_path", "extrainfo") def __init__(self, path: str | StorePath = "", extrainfo: Any = None) -> None: if isinstance(path, StorePath): self._path = path._path self.extrainfo = extrainfo or path.extrainfo else: self._path = self._strip_prefix(str(path)) self.extrainfo = extrainfo @staticmethod def _strip_prefix(path: str) -> str: """Remove the store directory, and refuse a path of another store. The refusal is the point. This kept an absolute path of another store whole, and `__str__` then put the store directory in front of it a second time. The result named no file, and nothing reported the mistake. Issue nixidae/nixidae#212 holds the measurement. """ prefix = store_prefix() if path.startswith(prefix): return path[len(prefix) :] if path.startswith("/"): from .store_dir import store_dir raise ValueError(f"{path!r} is not a path of the store at {store_dir()!r}") return path # ── the wire ─────────────────────────────────────────────────── @classmethod def from_wire(cls, value: str) -> Self: return cls(value)
[docs] def to_wire(self) -> str: """The whole path, which is what the daemon wire carries. `CommonProto::Serialise<StorePath>::write` of Nix calls `store.printStorePath`, and that is this. narinfo's `References:` calls `to_string()` instead, which is `name` below -- a different codec, not a differently annotated field. """ return str(self)
[docs] def to_json_value(self) -> str: """What JSON carries: the base name. `adl_serializer<nix::StorePath>::to_json`, `src/libstore/path.cc:95` of Nix, writes `storePath.to_string()`, and `from_json` builds one straight back from that string. So a `Realisation`, which travels as JSON, carries `abc-foo` where the binary wire carries `/nix/store/abc-foo`. **This is the whole of issue #4 in one pair of methods.** One value, two codecs, and the form belongs to the codec rather than to the value or to the field that holds it. """ return self.name
@classmethod def __get_pydantic_core_schema__(cls, _source: Any, _handler: Any) -> core_schema.CoreSchema: """Read either form, write the JSON one. The instance arm comes first so a `StorePath` passes through; a string goes to the constructor, which takes the base name or the whole path. """ return core_schema.union_schema( [ core_schema.is_instance_schema(cls), core_schema.no_info_after_validator_function( cls.from_wire, core_schema.str_schema(), ), ], serialization=core_schema.plain_serializer_function_ser_schema(cls.to_json_value), ) # ── accessors ────────────────────────────────────────────────── @property def path(self) -> str: """The whole path. The spelling most of pynixd already uses.""" return str(self)
[docs] def base(self) -> str: """The base name, without the store directory.""" return self._path
@property def name(self) -> str: """The base name: what Nix's `to_string()` answers.""" return Path(self._path).name
[docs] def hash_part(self) -> str: """The 32-character hash in front of the name.""" return self.name.split("-", 1)[0]
[docs] def base_name(self) -> str: """The readable part, after the hash.""" parts = self.name.split("-", 1) return parts[1] if len(parts) > 1 else ""
def is_derivation(self) -> bool: return self._path.endswith(".drv")
[docs] def to_path(self) -> Path: """The whole path, as a `pathlib.Path`.""" return Path(str(self))
[docs] def with_store_prefix(self) -> StorePath: """Return self: `__str__` already puts the store directory first.""" return self
# ── str-adjacent helpers, on the base name ───────────────────── def endswith(self, suffix: str) -> bool: return self._path.endswith(suffix) def startswith(self, prefix: str) -> bool: return self._path.startswith(prefix) # ── dunders ──────────────────────────────────────────────────── def __str__(self) -> str: if not self._path: return "" return store_prefix() + self._path def __repr__(self) -> str: inner = repr(self._path) if self.extrainfo: return f"StorePath({inner}, info={self.extrainfo!r})" return f"StorePath({inner})" def __eq__(self, other: object) -> bool: """Only another `StorePath`. Not a `str`. A path and its text are different things, and the whole reason this class exists is that there is more than one text for one path. `str(path) == text` says which text was meant. `extrainfo` is a debugging note and is not part of the value, so two paths that differ only there are equal and hash alike. """ if isinstance(other, StorePath): return self._path == other._path return NotImplemented def __hash__(self) -> int: return hash(self._path) def __bool__(self) -> bool: return bool(self._path) def __len__(self) -> int: return len(self._path) def __lt__(self, other: object) -> bool: if isinstance(other, StorePath): return self._path < other._path return NotImplemented def __le__(self, other: object) -> bool: if isinstance(other, StorePath): return self._path <= other._path return NotImplemented def __json__(self) -> str: return str(self)