From cd46536754b37bca365f67ddb474c6f32c2bfe51 Mon Sep 17 00:00:00 2001 From: Sean N Date: Fri, 17 Apr 2026 16:45:36 +0200 Subject: [PATCH] Added docstrings to all public api's --- .gitignore | 2 + nitro_validator/__init__.py | 29 ++++- nitro_validator/core/exceptions.py | 60 ++++++++-- nitro_validator/core/rule.py | 103 ++++++++++++---- nitro_validator/core/rule_registry.py | 91 +++++++++----- nitro_validator/core/validator.py | 166 ++++++++++++++++---------- nitro_validator/utils/__init__.py | 22 +++- pyproject.toml | 2 +- 8 files changed, 349 insertions(+), 126 deletions(-) diff --git a/.gitignore b/.gitignore index 8156093..9a569b5 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,5 @@ dmypy.json # Pyre type checker .pyre/ +CLAUDE.md +.claude.json diff --git a/nitro_validator/__init__.py b/nitro_validator/__init__.py index 3f45856..8f5971c 100644 --- a/nitro_validator/__init__.py +++ b/nitro_validator/__init__.py @@ -11,7 +11,7 @@ validated = validator.validate(data, rules) """ -__version__ = "1.0.2" +__version__ = "1.0.3" from .core import ( NitroValidator, @@ -87,11 +87,32 @@ class NitroValidator(_OriginalNitroValidator): + """Validate data against rules, with every built-in rule pre-registered. + + This is the public entry point exported at the top level of the + package. It is a thin subclass of + :class:`nitro_validator.core.NitroValidator` whose only job is to + default the registry to a shared instance pre-populated with all 51+ + built-in rules. Pass ``registry=`` to opt out of the defaults. + + Example: + >>> from nitro_validator import NitroValidator + >>> v = NitroValidator() + >>> v.validate( + ... {'email': 'a@b.co', 'age': 21}, + ... {'email': 'required|email', 'age': 'required|integer|min:18'}, + ... ) + {'email': 'a@b.co', 'age': 21} """ - NitroValidator with built-in rules pre-registered. - """ - def __init__(self, registry=None): + def __init__(self, registry: "NitroRuleRegistry" = None): + """Create a validator, defaulting to the shared built-in registry. + + Args: + registry: A custom :class:`NitroRuleRegistry`. Omit to reuse + the module-level registry that already contains every + built-in rule. + """ super().__init__(registry or _default_registry) diff --git a/nitro_validator/core/exceptions.py b/nitro_validator/core/exceptions.py index 3a906b7..2ffc1a7 100644 --- a/nitro_validator/core/exceptions.py +++ b/nitro_validator/core/exceptions.py @@ -2,33 +2,79 @@ Custom exception classes for nitro-validator. """ +from typing import Dict, List + class NitroValidatorException(Exception): - """Base exception for all validator errors.""" + """Base exception for every error raised by nitro-validator. + + Catch this when you want to trap anything the library throws without + caring which specific failure occurred. All other exceptions in the + package subclass this one. + + Example: + >>> try: + ... validator.validate(data, rules) + ... except NitroValidatorException: + ... # catches ValidationError, RuleNotFoundError, InvalidRuleError + ... pass + """ pass class NitroValidationError(NitroValidatorException): - """ - Raised when validation fails. + """Raise when one or more fields fail validation. + + The `errors` attribute maps each failing field name to a list of + human-readable error messages. Rules that pass do not appear. This is + the exception raised by :meth:`NitroValidator.validate` on failure. Attributes: - errors: Dictionary of field names to error messages + errors: Dict mapping field name to a list of error messages for + that field. + + Example: + >>> try: + ... validator.validate({'email': ''}, {'email': 'required|email'}) + ... except NitroValidationError as e: + ... print(e.errors) + {'email': ['The email field is required.']} """ - def __init__(self, errors): + def __init__(self, errors: Dict[str, List[str]]): + """Build the exception with a field-keyed error map. + + Args: + errors: Dict of field name to list of error strings. + """ self.errors = errors super().__init__(f"Validation failed: {errors}") class NitroRuleNotFoundError(NitroValidatorException): - """Raised when a validation rule is not found.""" + """Raise when a rule name is referenced but has not been registered. + + Typically surfaced when a rule string like ``'my_custom_rule'`` is + used before the corresponding rule class is registered on the + validator's registry. + + Example: + >>> validator.validate({'x': 1}, {'x': 'not_a_real_rule'}) + Traceback (most recent call last): + ... + NitroRuleNotFoundError: Rule 'not_a_real_rule' not found in registry + """ pass class NitroInvalidRuleError(NitroValidatorException): - """Raised when a rule is invalid or improperly defined.""" + """Raise when a rule class is malformed or fails registration checks. + + Thrown by :meth:`NitroRuleRegistry.register` when the supplied class + does not inherit from :class:`NitroValidationRule` or lacks a usable + ``name`` attribute. + """ pass diff --git a/nitro_validator/core/rule.py b/nitro_validator/core/rule.py index 0ce8fc0..e967db2 100644 --- a/nitro_validator/core/rule.py +++ b/nitro_validator/core/rule.py @@ -6,50 +6,113 @@ class NitroValidationRule: - """ - Base class for all validation rules. - - Custom rules should inherit from NitroValidationRule and implement the validate() method. + """Base class for every validation rule in nitro-validator. + + Subclass this to add a new rule. Set two class attributes and + implement :meth:`validate`: + + * ``name`` — the string used in pipe-delimited rule syntax + (e.g. ``"required"``, ``"min"``). + * ``message`` — the default error template. Placeholders ``{field}``, + ``{args}``, and positional ``{0}``, ``{1}``, ... are substituted + by :meth:`get_message`. + + A rule instance carries its positional arguments in ``self.args`` — + these come from the colon/comma-delimited tail of a rule string + (``"between:1,100"`` → ``args == ("1", "100")``), always as strings + when parsed from a rule string, so cast inside :meth:`validate` if + you need numbers. + + Convention: return ``True`` for ``None`` or empty string in every rule + *except* presence checks (``required``, ``accepted``, ``declined``). + This lets callers compose rules like ``"email"`` with an optional + field by omitting ``required``. + + Attributes: + name: Class-level identifier used by the rule registry. + message: Class-level default error template. + args: Positional arguments the rule was instantiated with. + kwargs: Keyword arguments (reserved; ``message`` is extracted). + custom_message: Per-instance override applied by the validator + when the caller passes custom messages. + + Example: + >>> from nitro_validator import NitroValidationRule, NitroValidator + >>> class StartsUpperRule(NitroValidationRule): + ... name = "starts_upper" + ... message = "The {field} field must start with an uppercase letter." + ... def validate(self, field, value, data): + ... if not value: + ... return True # let `required` enforce presence + ... return isinstance(value, str) and value[:1].isupper() + >>> v = NitroValidator() + >>> v.register_rule(StartsUpperRule) + >>> v.is_valid({'name': 'Alice'}, {'name': 'starts_upper'}) + True """ name = None # Rule name (e.g., 'required', 'email', 'min') message = "The {field} field is invalid." # Default error message - def __init__(self, *args, **kwargs): - """ - Initialize the rule with optional parameters. + def __init__(self, *args: Any, **kwargs: Any): + """Store the rule's positional and keyword arguments. + + Rule strings like ``"between:1,100"`` are parsed into + ``*args = ("1", "100")`` — arguments therefore arrive as + strings and must be cast inside :meth:`validate`. Pass + ``message=`` to override the default template for this + instance only. Args: - *args: Positional arguments for the rule (e.g., min value, max value) - **kwargs: Keyword arguments (e.g., custom error message) + *args: Positional rule arguments (e.g. min value, pattern). + **kwargs: Reserved keyword options. ``message`` becomes + ``self.custom_message``. """ self.args = args self.kwargs = kwargs self.custom_message = kwargs.get("message") def validate(self, field: str, value: Any, data: dict) -> bool: - """ - Validate the field value. + """Return ``True`` if ``value`` passes this rule, otherwise ``False``. + + Subclasses must override this method. The full ``data`` dict is + provided so cross-field rules (``same``, ``confirmed``, ...) can + peek at sibling values. Args: - field: The field name being validated - value: The value to validate - data: The complete data dictionary (for cross-field validation) + field: Name of the field being validated. + value: The value to check. + data: The full data dict under validation. Returns: - True if validation passes, False otherwise + ``True`` to accept the value, ``False`` to mark the field as + failing this rule. + + Raises: + NotImplementedError: If a subclass does not override this. """ raise NotImplementedError("Subclasses must implement validate()") def get_message(self, field: str) -> str: - """ - Get the error message for this rule. + """Return the formatted error message for a failing field. + + Uses ``self.custom_message`` when set (e.g. via the validator's + ``messages`` argument), otherwise the class-level ``message``. + Substitutes ``{field}``, ``{args}`` (comma-joined), and + positional placeholders ``{0}``, ``{1}``, ... from ``self.args``. Args: - field: The field name being validated + field: Name of the failing field to substitute into the + message template. Returns: - The formatted error message + The rendered error message. + + Example: + >>> rule = NitroValidationRule("18") + >>> rule.message = "The {field} must be at least {0}." + >>> rule.get_message("age") + 'The age must be at least 18.' """ message = self.custom_message or self.message @@ -68,6 +131,6 @@ def get_message(self, field: str) -> str: return message - def __repr__(self): + def __repr__(self) -> str: args_str = ", ".join(str(arg) for arg in self.args) if self.args else "" return f"{self.__class__.__name__}({args_str})" diff --git a/nitro_validator/core/rule_registry.py b/nitro_validator/core/rule_registry.py index d375529..20c8a21 100644 --- a/nitro_validator/core/rule_registry.py +++ b/nitro_validator/core/rule_registry.py @@ -8,26 +8,47 @@ class NitroRuleRegistry: + """Store and look up validation rule classes by name. + + Rule strings like ``"required|email"`` are resolved against a + registry: each name is mapped to a :class:`NitroValidationRule` + subclass, which the validator then instantiates. Each + :class:`NitroValidator` has its own registry, pre-populated with the + 51+ built-in rules. Use this class directly when you want a + container isolated from the default built-ins. + + Example: + >>> from nitro_validator import NitroRuleRegistry, NitroValidator + >>> registry = NitroRuleRegistry() + >>> registry.register(MyCustomRule) + >>> validator = NitroValidator(registry=registry) """ - Registry for storing and retrieving validation rules. - This allows users to register custom rules and access built-in rules. - """ - - def __init__(self): - """Initialize the rule registry.""" + def __init__(self) -> None: + """Create an empty registry with no rules registered.""" self._rules: Dict[str, Type[NitroValidationRule]] = {} - def register(self, rule_class: Type[NitroValidationRule], name: Optional[str] = None): - """ - Register a validation rule. + def register( + self, + rule_class: Type[NitroValidationRule], + name: Optional[str] = None, + ) -> None: + """Register a rule class under its ``name`` attribute or an override. Args: - rule_class: The NitroValidationRule class to register - name: Optional custom name for the rule (defaults to rule_class.name) + rule_class: A :class:`NitroValidationRule` subclass to add. + name: Optional name override. Defaults to ``rule_class.name``. Raises: - NitroInvalidRuleError: If the rule class is invalid + NitroInvalidRuleError: If ``rule_class`` does not subclass + :class:`NitroValidationRule`, or if neither ``name`` nor + ``rule_class.name`` is set. + + Example: + >>> registry = NitroRuleRegistry() + >>> registry.register(EmailRule) + >>> registry.has("email") + True """ if not issubclass(rule_class, NitroValidationRule): raise NitroInvalidRuleError(f"{rule_class} must inherit from NitroValidationRule") @@ -41,28 +62,37 @@ def register(self, rule_class: Type[NitroValidationRule], name: Optional[str] = self._rules[rule_name] = rule_class - def unregister(self, name: str): - """ - Unregister a validation rule. + def unregister(self, name: str) -> None: + """Remove a rule from the registry. No-op if it is not registered. Args: - name: The name of the rule to unregister + name: The rule name to remove. + + Example: + >>> registry.unregister("email") + >>> registry.has("email") + False """ if name in self._rules: del self._rules[name] def get(self, name: str) -> Type[NitroValidationRule]: - """ - Get a rule class by name. + """Return the rule class registered under ``name``. Args: - name: The name of the rule + name: The rule name to look up. Returns: - The NitroValidationRule class + The :class:`NitroValidationRule` subclass registered for + ``name``. Raises: - NitroRuleNotFoundError: If the rule is not found + NitroRuleNotFoundError: If no rule is registered under + that name. + + Example: + >>> registry.get("email") is EmailRule + True """ if name not in self._rules: raise NitroRuleNotFoundError(f"Rule '{name}' not found in registry") @@ -70,26 +100,27 @@ def get(self, name: str) -> Type[NitroValidationRule]: return self._rules[name] def has(self, name: str) -> bool: - """ - Check if a rule exists in the registry. + """Return ``True`` if a rule is registered under ``name``. Args: - name: The name of the rule + name: The rule name to check. Returns: - True if the rule exists, False otherwise + ``True`` if ``name`` is registered, ``False`` otherwise. """ return name in self._rules def all(self) -> Dict[str, Type[NitroValidationRule]]: - """ - Get all registered rules. + """Return a shallow copy of the name-to-class mapping. + + The returned dict is detached from the registry, so mutating it + does not affect registration. Returns: - Dictionary of rule names to NitroValidationRule classes + A fresh dict mapping each registered rule name to its class. """ return self._rules.copy() - def clear(self): - """Clear all registered rules.""" + def clear(self) -> None: + """Remove every registered rule. Useful for building a registry from scratch.""" self._rules.clear() diff --git a/nitro_validator/core/validator.py b/nitro_validator/core/validator.py index e889c50..68ddba9 100644 --- a/nitro_validator/core/validator.py +++ b/nitro_validator/core/validator.py @@ -9,38 +9,66 @@ class NitroValidator: - """ - Main validator class for validating data against rules. + """Validate a data dict against a per-field rule spec. + + The validator resolves rule names through a :class:`NitroRuleRegistry`, + runs every rule for every field, aggregates errors by field, and + either returns the validated subset or raises + :class:`NitroValidationError`. + + Rules can be expressed two ways — interchangeably within the same + call: + + * Pipe-delimited strings: ``"required|email"``, ``"min:18"``, + ``"between:1,100"``. Colon separates the rule name from its + arguments; commas separate multiple arguments. + * Rule instances or classes: ``[RequiredRule(), EmailRule()]``. + + Every :meth:`validate` call resets :attr:`errors` and + :attr:`validated_data`, so a single validator can be reused across + requests. Example: - validator = NitroValidator() - validator.validate( - {'email': 'user@example.com', 'age': 25}, - {'email': 'required|email', 'age': 'required|numeric|min:18'} - ) + >>> from nitro_validator import NitroValidator + >>> validator = NitroValidator() + >>> validator.validate( + ... {'email': 'user@example.com', 'age': 25}, + ... {'email': 'required|email', 'age': 'required|integer|min:18'}, + ... ) + {'email': 'user@example.com', 'age': 25} """ def __init__(self, registry: Optional[NitroRuleRegistry] = None): - """ - Initialize the validator. + """Create a validator backed by ``registry`` (or a fresh empty one). + + When imported from the top-level :mod:`nitro_validator` package, + the default registry already contains every built-in rule. Args: - registry: Optional custom NitroRuleRegistry instance + registry: A :class:`NitroRuleRegistry` to resolve rule names + against. Omit to use the default registry populated with + built-in rules. """ self.registry = registry or NitroRuleRegistry() self.errors: Dict[str, List[str]] = {} self.validated_data: Dict[str, Any] = {} - def register_rule(self, rule_class: type, name: Optional[str] = None): - """ - Register a custom validation rule. + def register_rule( + self, + rule_class: type, + name: Optional[str] = None, + ) -> "NitroValidator": + """Register a custom rule class on this validator's registry. Args: - rule_class: The Rule class to register - name: Optional custom name for the rule + rule_class: A :class:`NitroValidationRule` subclass. + name: Optional override for the rule name. Returns: - Self for method chaining + Self, to allow chaining. + + Example: + >>> validator = NitroValidator().register_rule(StrongPasswordRule) """ self.registry.register(rule_class, name) return self @@ -51,38 +79,37 @@ def validate( rules: Dict[str, Union[str, List[Union[str, NitroValidationRule]]]], messages: Optional[Dict[str, Union[str, Dict[str, str]]]] = None, ) -> Dict[str, Any]: - """ - Validate data against rules. + """Run every rule for every field and return the validated data. + + Every rule for every field is evaluated before this method + raises, so :attr:`errors` contains *all* failures, not just the + first. Fields absent from ``data`` are validated against their + rules with a value of ``None`` — most rules pass on ``None``, so + pair with ``required`` to enforce presence. Args: - data: The data to validate - rules: Dictionary of field names to validation rules - messages: Optional custom error messages + data: The payload to validate. + rules: Per-field rules. Values may be pipe-delimited strings + (``"required|email"``) or lists mixing rule strings and + rule instances. + messages: Optional custom error messages. Each value is + either a single string applied to all rules on that + field, or a dict keyed by rule name for per-rule + overrides. Returns: - The validated data + The subset of ``data`` whose fields passed every rule. Raises: - NitroValidationError: If validation fails + NitroValidationError: If any field failed any rule; the + exception's ``errors`` attribute holds the full report. Example: - validator.validate( - {'email': 'test@example.com'}, - {'email': 'required|email'} - ) - - # Or with rule objects - validator.validate( - {'age': 25}, - {'age': [RequiredRule(), NumericRule(), MinRule(18)]} - ) - - # Or with custom messages - validator.validate( - {'email': ''}, - {'email': 'required|email'}, - {'email': 'Please provide a valid email address'} - ) + >>> validator.validate( + ... {'email': 'test@example.com'}, + ... {'email': 'required|email'}, + ... ) + {'email': 'test@example.com'} """ self.errors = {} self.validated_data = {} @@ -140,16 +167,22 @@ def is_valid( rules: Dict[str, Union[str, List[Union[str, NitroValidationRule]]]], messages: Optional[Dict[str, Union[str, Dict[str, str]]]] = None, ) -> bool: - """ - Check if data is valid without raising an exception. + """Return ``True`` if ``data`` satisfies ``rules``, without raising. + + Wraps :meth:`validate` and swallows :class:`NitroValidationError`. + On failure call :meth:`get_errors` to inspect the result. Args: - data: The data to validate - rules: Dictionary of field names to validation rules - messages: Optional custom error messages + data: The payload to validate. + rules: Per-field rules (same format as :meth:`validate`). + messages: Optional custom error messages. Returns: - True if valid, False otherwise + ``True`` if every field passed every rule, ``False`` otherwise. + + Example: + >>> if not validator.is_valid(data, rules): + ... print(validator.get_errors()) """ try: self.validate(data, rules, messages) @@ -158,20 +191,23 @@ def is_valid( return False def get_errors(self) -> Dict[str, List[str]]: - """ - Get validation errors. + """Return the errors from the most recent :meth:`validate` call. Returns: - Dictionary of field names to error messages + A dict mapping field name to the list of error messages + accumulated for that field. Empty when the last validation + succeeded. """ return self.errors def get_errors_flat(self) -> List[str]: - """ - Get all error messages as a flat list. + """Return every error message as a flat list, unkeyed by field. + + Preserves the insertion order from :attr:`errors`. Useful when + surfacing validation failures as a single bullet list in a UI. Returns: - List of all error messages + A flat list of every error message across every field. """ flat_errors = [] for field_errors in self.errors.values(): @@ -225,20 +261,30 @@ def make( messages: Optional[Dict[str, Union[str, Dict[str, str]]]] = None, registry: Optional[NitroRuleRegistry] = None, ) -> "NitroValidator": - """ - Factory method to create a validator and validate data in one call. + """Construct a validator and run :meth:`validate` in one call. + + Convenient when you only want a one-shot validation and the + validator instance afterwards — e.g. to read + :attr:`validated_data`. Args: - data: The data to validate - rules: Dictionary of field names to validation rules - messages: Optional custom error messages - registry: Optional custom NitroRuleRegistry instance + data: The payload to validate. + rules: Per-field rules (same format as :meth:`validate`). + messages: Optional custom error messages. + registry: Optional custom :class:`NitroRuleRegistry`. Returns: - NitroValidator instance with validation results + The validator instance, with :attr:`validated_data` populated. Raises: - NitroValidationError: If validation fails + NitroValidationError: If validation fails. + + Example: + >>> validator = NitroValidator.make( + ... {'email': 'a@b.co'}, {'email': 'required|email'}, + ... ) + >>> validator.validated_data + {'email': 'a@b.co'} """ validator = cls(registry) validator.validate(data, rules, messages) diff --git a/nitro_validator/utils/__init__.py b/nitro_validator/utils/__init__.py index 822006e..bde91e7 100644 --- a/nitro_validator/utils/__init__.py +++ b/nitro_validator/utils/__init__.py @@ -2,6 +2,8 @@ Utility functions and built-in rules for nitro-validator. """ +from ..core.rule_registry import NitroRuleRegistry + from .rules import ( # Basic rules RequiredRule, @@ -121,12 +123,24 @@ ] -def register_builtin_rules(registry): - """ - Register all built-in validation rules with a registry. +def register_builtin_rules(registry: NitroRuleRegistry) -> None: + """Register every built-in validation rule onto ``registry``. + + The default registry returned by :class:`NitroValidator() + ` is already populated with these + rules — call this only when you are building a custom registry + from scratch and want the built-ins available under their standard + names. Args: - registry: RuleRegistry instance to register rules with + registry: A :class:`NitroRuleRegistry` instance to mutate. + + Example: + >>> from nitro_validator import NitroRuleRegistry, register_builtin_rules + >>> registry = NitroRuleRegistry() + >>> register_builtin_rules(registry) + >>> registry.has("email") + True """ rules = [ RequiredRule, diff --git a/pyproject.toml b/pyproject.toml index e28b428..bc5f36f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nitro-validator" -version = "1.0.2" +version = "1.0.3" description = "A powerful, standalone, dependency-free data validation library for Python with extensible rules and a clean, intuitive API." readme = "README.md" authors = [