From d547f3510d5e7df45b2266b354d9cc0aa7a3943b Mon Sep 17 00:00:00 2001 From: Ryan Smith Date: Wed, 7 Jan 2026 11:22:38 -0500 Subject: [PATCH 1/3] extensions/attrs: Add validation to the epoch field for nevr and nevra --- src/pkgps/_impl/attrs/nevr.py | 4 ++-- src/pkgps/_impl/attrs/nevra.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pkgps/_impl/attrs/nevr.py b/src/pkgps/_impl/attrs/nevr.py index 5c0c959..5a863c5 100644 --- a/src/pkgps/_impl/attrs/nevr.py +++ b/src/pkgps/_impl/attrs/nevr.py @@ -3,7 +3,7 @@ __all__ = ["NEVR"] from attrs import asdict, evolve, field, frozen -from attrs.validators import matches_re +from attrs.validators import ge, matches_re from typing_extensions import Self from ..parse import parse_nevr, parse_nevr_or_none @@ -31,7 +31,7 @@ class NEVR: ) ) """Name. Must not contain: < > = whitespace { } %""" - epoch: int = 0 + epoch: int = field(default=0, validator=ge(0)) """Epoch.""" version: str """Version.""" diff --git a/src/pkgps/_impl/attrs/nevra.py b/src/pkgps/_impl/attrs/nevra.py index 2c79380..bac1b43 100644 --- a/src/pkgps/_impl/attrs/nevra.py +++ b/src/pkgps/_impl/attrs/nevra.py @@ -3,7 +3,7 @@ __all__ = ["NEVRA"] from attrs import asdict, evolve, field, frozen -from attrs.validators import matches_re +from attrs.validators import ge, matches_re from ..parse import parse_nevra, parse_nevra_or_none from .nevr import NEVR @@ -32,7 +32,7 @@ class NEVRA: ) ) """Name. Must not contain: < > = whitespace { } %""" - epoch: int = 0 + epoch: int = field(default=0, validator=ge(0)) """Epoch.""" version: str """Version.""" From 481b0d27e2a612bf748e6310fce461fffe58ea56 Mon Sep 17 00:00:00 2001 From: Ryan Smith Date: Wed, 7 Jan 2026 11:29:34 -0500 Subject: [PATCH 2/3] _impl/base: Be consistent about using internal fields for internally --- src/pkgps/_impl/nevr.py | 14 +++++++------- src/pkgps/_impl/nevra.py | 20 +++++++++++--------- src/pkgps/_impl/nvr.py | 10 +++++----- src/pkgps/_impl/nvra.py | 16 ++++++++-------- 4 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/pkgps/_impl/nevr.py b/src/pkgps/_impl/nevr.py index 3e9e913..5b22396 100644 --- a/src/pkgps/_impl/nevr.py +++ b/src/pkgps/_impl/nevr.py @@ -54,23 +54,23 @@ def __eq__(self, other: object) -> bool: return ( isinstance(other, NEVR) and self._name == other.name - and self.epoch == other.epoch - and self.version == other.version - and self.release == other.release + and self._epoch == other.epoch + and self._version == other.version + and self._release == other.release ) def __hash__(self) -> int: - return hash((self._name, self.epoch, self.version, self.release)) + return hash((self._name, self._epoch, self._version, self._release)) def __repr__(self) -> str: return f"NEVR(name={self._name!r}, epoch={self._epoch!r}, version={self._version!r}, release={self._release!r})" def __str__(self) -> str: - epoch_string = f"{self.epoch}:" if self.epoch else "" - return f"{self.name}-{epoch_string}{self.version}-{self.release}" + epoch_string = f"{self._epoch}:" if self._epoch else "" + return f"{self._name}-{epoch_string}{self._version}-{self._release}" def __iter__(self): - return iter((self.name, self.epoch, self.version, self.release)) + return iter((self._name, self._epoch, self._version, self._release)) @property def name(self) -> str: diff --git a/src/pkgps/_impl/nevra.py b/src/pkgps/_impl/nevra.py index 05f42e1..1bd48c2 100644 --- a/src/pkgps/_impl/nevra.py +++ b/src/pkgps/_impl/nevra.py @@ -56,25 +56,27 @@ def from_string_or_none(cls, nevra: str | None) -> NEVRA | None: def __eq__(self, other: object) -> bool: return ( isinstance(other, NEVRA) - and self.name == other.name - and self.epoch == other.epoch - and self.version == other.version - and self.release == other.release - and self.arch == other.arch + and self._name == other.name + and self._epoch == other.epoch + and self._version == other.version + and self._release == other.release + and self._arch == other.arch ) def __hash__(self) -> int: - return hash((self._name, self.epoch, self.version, self.release, self.arch)) + return hash((self._name, self._epoch, self._version, self._release, self._arch)) def __repr__(self) -> str: return f"NEVRA(name={self._name!r}, epoch={self._epoch!r}, version={self._version!r}, release={self._release!r}, arch={self._arch!r})" def __str__(self) -> str: - epoch_string = f"{self.epoch}:" if self.epoch else "" - return f"{self.name}-{epoch_string}{self.version}-{self.release}.{self.arch}" + epoch_string = f"{self._epoch}:" if self._epoch else "" + return ( + f"{self._name}-{epoch_string}{self._version}-{self._release}.{self._arch}" + ) def __iter__(self): - return iter((self.name, self.epoch, self.version, self.release, self.arch)) + return iter((self._name, self._epoch, self._version, self._release, self._arch)) @property def name(self) -> str: diff --git a/src/pkgps/_impl/nvr.py b/src/pkgps/_impl/nvr.py index bdae34d..90f28d1 100644 --- a/src/pkgps/_impl/nvr.py +++ b/src/pkgps/_impl/nvr.py @@ -56,21 +56,21 @@ def __eq__(self, other: object) -> bool: return ( isinstance(other, NVR) and self._name == other.name - and self.version == other.version - and self.release == other.release + and self._version == other.version + and self._release == other.release ) def __hash__(self) -> int: - return hash((self._name, self.version, self.release)) + return hash((self._name, self._version, self._release)) def __repr__(self) -> str: return f"NVR(name={self._name!r}, version={self._version!r}, release={self._release!r})" def __str__(self) -> str: - return f"{self.name}-{self.version}-{self.release}" + return f"{self._name}-{self._version}-{self._release}" def __iter__(self) -> Iterator[str]: - return iter((self.name, self.version, self.release)) + return iter((self._name, self._version, self._release)) @property def name(self) -> str: diff --git a/src/pkgps/_impl/nvra.py b/src/pkgps/_impl/nvra.py index 0ab889a..ccd9f6d 100644 --- a/src/pkgps/_impl/nvra.py +++ b/src/pkgps/_impl/nvra.py @@ -50,23 +50,23 @@ def from_string_or_none(cls, nvra: str | None) -> NVRA | None: def __eq__(self, other: object) -> bool: return ( isinstance(other, NVRA) - and self.name == other.name - and self.version == other.version - and self.release == other.release - and self.arch == other.arch + and self._name == other.name + and self._version == other.version + and self._release == other.release + and self._arch == other.arch ) def __hash__(self) -> int: - return hash((self.name, self.version, self.release, self.arch)) + return hash((self._name, self._version, self._release, self._arch)) def __repr__(self) -> str: - return f"NVRA(name={self.name!r}, version={self.version!r}, release={self.release!r}, arch={self.arch!r})" + return f"NVRA(name={self._name!r}, version={self._version!r}, release={self._release!r}, arch={self._arch!r})" def __str__(self) -> str: - return f"{self.name}-{self.version}-{self.release}.{self.arch}" + return f"{self._name}-{self._version}-{self._release}.{self._arch}" def __iter__(self): - return iter((self.name, self.version, self.release, self.arch)) + return iter((self._name, self._version, self._release, self._arch)) @property def name(self) -> str: From 1719ac35b5fa63ec00e3042ae2d1178560528d46 Mon Sep 17 00:00:00 2001 From: Ryan Smith Date: Wed, 7 Jan 2026 16:57:19 -0500 Subject: [PATCH 3/3] Add validation to all types so that version and release are non-empty --- src/pkgps/_impl/attrs/nevr.py | 6 +++--- src/pkgps/_impl/attrs/nevra.py | 6 +++--- src/pkgps/_impl/attrs/nvr.py | 6 +++--- src/pkgps/_impl/attrs/nvra.py | 6 +++--- src/pkgps/_impl/nevr.py | 2 ++ src/pkgps/_impl/nevra.py | 2 ++ src/pkgps/_impl/nvr.py | 2 ++ src/pkgps/_impl/nvra.py | 2 ++ src/pkgps/_impl/pydantic/nevr.py | 4 ++-- src/pkgps/_impl/pydantic/nevra.py | 4 ++-- src/pkgps/_impl/pydantic/nvr.py | 4 ++-- src/pkgps/_impl/pydantic/nvra.py | 4 ++-- 12 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/pkgps/_impl/attrs/nevr.py b/src/pkgps/_impl/attrs/nevr.py index 5a863c5..adb32fa 100644 --- a/src/pkgps/_impl/attrs/nevr.py +++ b/src/pkgps/_impl/attrs/nevr.py @@ -3,7 +3,7 @@ __all__ = ["NEVR"] from attrs import asdict, evolve, field, frozen -from attrs.validators import ge, matches_re +from attrs.validators import ge, matches_re, min_len from typing_extensions import Self from ..parse import parse_nevr, parse_nevr_or_none @@ -33,9 +33,9 @@ class NEVR: """Name. Must not contain: < > = whitespace { } %""" epoch: int = field(default=0, validator=ge(0)) """Epoch.""" - version: str + version: str = field(validator=min_len(1)) """Version.""" - release: str + release: str = field(validator=min_len(1)) """Release.""" copy = evolve diff --git a/src/pkgps/_impl/attrs/nevra.py b/src/pkgps/_impl/attrs/nevra.py index bac1b43..8fe4754 100644 --- a/src/pkgps/_impl/attrs/nevra.py +++ b/src/pkgps/_impl/attrs/nevra.py @@ -3,7 +3,7 @@ __all__ = ["NEVRA"] from attrs import asdict, evolve, field, frozen -from attrs.validators import ge, matches_re +from attrs.validators import ge, matches_re, min_len from ..parse import parse_nevra, parse_nevra_or_none from .nevr import NEVR @@ -34,9 +34,9 @@ class NEVRA: """Name. Must not contain: < > = whitespace { } %""" epoch: int = field(default=0, validator=ge(0)) """Epoch.""" - version: str + version: str = field(validator=min_len(1)) """Version.""" - release: str + release: str = field(validator=min_len(1)) """Release.""" arch: str """Architecture.""" diff --git a/src/pkgps/_impl/attrs/nvr.py b/src/pkgps/_impl/attrs/nvr.py index 6898f43..32aa4ec 100644 --- a/src/pkgps/_impl/attrs/nvr.py +++ b/src/pkgps/_impl/attrs/nvr.py @@ -4,7 +4,7 @@ from attrs import asdict, evolve, field, frozen -from attrs.validators import matches_re +from attrs.validators import matches_re, min_len from ..parse import parse_nvr, parse_nvr_or_none @@ -31,9 +31,9 @@ class NVR: ) ) """Name. Must not contain: < > = whitespace { } %""" - version: str + version: str = field(validator=min_len(1)) """Version.""" - release: str + release: str = field(validator=min_len(1)) """Release.""" copy = evolve diff --git a/src/pkgps/_impl/attrs/nvra.py b/src/pkgps/_impl/attrs/nvra.py index 50e48fa..d8cbf4e 100644 --- a/src/pkgps/_impl/attrs/nvra.py +++ b/src/pkgps/_impl/attrs/nvra.py @@ -3,7 +3,7 @@ __all__ = ["NVRA"] from attrs import asdict, evolve, field, frozen -from attrs.validators import matches_re +from attrs.validators import matches_re, min_len from ..parse import parse_nvra, parse_nvra_or_none from .nvr import NVR @@ -30,9 +30,9 @@ class NVRA: ) ) """Name. Must not contain: < > = whitespace { } %""" - version: str + version: str = field(validator=min_len(1)) """Version.""" - release: str + release: str = field(validator=min_len(1)) """Release.""" arch: str """Architecture.""" diff --git a/src/pkgps/_impl/nevr.py b/src/pkgps/_impl/nevr.py index 5b22396..597e09e 100644 --- a/src/pkgps/_impl/nevr.py +++ b/src/pkgps/_impl/nevr.py @@ -36,6 +36,8 @@ def __init__( f"Prohibited characters: {INVALID_NAME_CHARS_DISPLAY}", ) require(epoch >= 0, "epoch must be a positive integer") + require(len(version) > 0, "Version cannot be empty") + require(len(release) > 0, "Release cannot be empty") self._name = name self._epoch = epoch diff --git a/src/pkgps/_impl/nevra.py b/src/pkgps/_impl/nevra.py index 1bd48c2..0d9758a 100644 --- a/src/pkgps/_impl/nevra.py +++ b/src/pkgps/_impl/nevra.py @@ -38,6 +38,8 @@ def __init__( f"Prohibited characters: {INVALID_NAME_CHARS_DISPLAY}", ) require(epoch >= 0, "epoch must be a positive integer") + require(len(version) > 0, "Version cannot be empty") + require(len(release) > 0, "Release cannot be empty") self._name = name self._epoch = epoch diff --git a/src/pkgps/_impl/nvr.py b/src/pkgps/_impl/nvr.py index 90f28d1..2cc96c9 100644 --- a/src/pkgps/_impl/nvr.py +++ b/src/pkgps/_impl/nvr.py @@ -39,6 +39,8 @@ def __init__(self, *, name: str, version: str, release: str) -> None: f"Package name '{name}' contains invalid character {invalid_match.group() if invalid_match else ''}. " f"Prohibited characters: {INVALID_NAME_CHARS_DISPLAY}", ) + require(len(version) > 0, "Version cannot be empty") + require(len(release) > 0, "Release cannot be empty") self._name = name self._version = version diff --git a/src/pkgps/_impl/nvra.py b/src/pkgps/_impl/nvra.py index ccd9f6d..c968d94 100644 --- a/src/pkgps/_impl/nvra.py +++ b/src/pkgps/_impl/nvra.py @@ -33,6 +33,8 @@ def __init__(self, *, name: str, version: str, release: str, arch: str) -> None: f"Package name '{name}' contains invalid character {invalid_match.group() if invalid_match else ''}. " f"Prohibited characters: {INVALID_NAME_CHARS_DISPLAY}", ) + require(len(version) > 0, "Version cannot be empty") + require(len(release) > 0, "Release cannot be empty") self._name = name self._version = version diff --git a/src/pkgps/_impl/pydantic/nevr.py b/src/pkgps/_impl/pydantic/nevr.py index eacebb4..fcbee90 100644 --- a/src/pkgps/_impl/pydantic/nevr.py +++ b/src/pkgps/_impl/pydantic/nevr.py @@ -30,9 +30,9 @@ class NEVR(Frozen): """Name. Must not contain: < > = whitespace { } %""" epoch: int = Field(default=0, ge=0) """Epoch.""" - version: str + version: str = Field(min_length=1) """Version.""" - release: str + release: str = Field(min_length=1) """Release.""" @classmethod diff --git a/src/pkgps/_impl/pydantic/nevra.py b/src/pkgps/_impl/pydantic/nevra.py index 8370dc5..b2a719d 100644 --- a/src/pkgps/_impl/pydantic/nevra.py +++ b/src/pkgps/_impl/pydantic/nevra.py @@ -32,9 +32,9 @@ class NEVRA(Frozen): """Name. Must not contain: < > = whitespace { } %""" epoch: int = Field(default=0, ge=0) """Epoch.""" - version: str + version: str = Field(min_length=1) """Version.""" - release: str + release: str = Field(min_length=1) """Release.""" arch: str """Architecture.""" diff --git a/src/pkgps/_impl/pydantic/nvr.py b/src/pkgps/_impl/pydantic/nvr.py index cad410a..62e177c 100644 --- a/src/pkgps/_impl/pydantic/nvr.py +++ b/src/pkgps/_impl/pydantic/nvr.py @@ -27,9 +27,9 @@ class NVR(Frozen): name: str = Field(pattern=r"^[^<>=\s{}%]+$") """Name. Must not contain: < > = whitespace { } %""" - version: str + version: str = Field(min_length=1) """Version.""" - release: str + release: str = Field(min_length=1) """Release.""" @classmethod diff --git a/src/pkgps/_impl/pydantic/nvra.py b/src/pkgps/_impl/pydantic/nvra.py index ab4cbfc..87d6d14 100644 --- a/src/pkgps/_impl/pydantic/nvra.py +++ b/src/pkgps/_impl/pydantic/nvra.py @@ -28,9 +28,9 @@ class NVRA(Frozen): name: str = Field(pattern=r"^[^<>=\s{}%]+$") """Name. Must not contain: < > = whitespace { } %""" - version: str + version: str = Field(min_length=1) """Version.""" - release: str + release: str = Field(min_length=1) """Release.""" arch: str """Architecture."""