Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/date_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ def example_date_format():
validator = Validator()

data = {
"uk_date": "15/06/1990", # DD/MM/YYYY
"us_date": "06-15-1990", # MM-DD-YYYY
"uk_date": "15/06/1990", # DD/MM/YYYY
"us_date": "06-15-1990", # MM-DD-YYYY
"timestamp": "15 Jun 1990 09:30", # DD Mon YYYY HH:MM
}
rules = {
Expand Down
23 changes: 14 additions & 9 deletions nitro_validator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
validated = validator.validate(data, rules)
"""

__version__ = "1.0.3"
__version__ = "1.0.4"

from typing import Optional

from .core import (
NitroValidator,
Expand Down Expand Up @@ -92,8 +94,11 @@ class NitroValidator(_OriginalNitroValidator):
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.
default the registry to a fresh copy of the module-level registry,
which is pre-populated with all 51+ built-in rules. Each
``NitroValidator()`` therefore gets its own registry, so
``register_rule()`` calls are isolated to that instance. Pass
``registry=`` to supply your own.

Example:
>>> from nitro_validator import NitroValidator
Expand All @@ -105,15 +110,15 @@ class NitroValidator(_OriginalNitroValidator):
{'email': 'a@b.co', 'age': 21}
"""

def __init__(self, registry: "NitroRuleRegistry" = None):
"""Create a validator, defaulting to the shared built-in registry.
def __init__(self, registry: Optional["NitroRuleRegistry"] = None):
"""Create a validator, defaulting to a fresh copy of the built-in registry.

Args:
registry: A custom :class:`NitroRuleRegistry`. Omit to reuse
the module-level registry that already contains every
built-in rule.
registry: A custom :class:`NitroRuleRegistry`. Omit to get a
fresh registry seeded with every built-in rule; rules
you register on this validator will not leak to others.
"""
super().__init__(registry or _default_registry)
super().__init__(registry if registry is not None else _default_registry.copy())


# Provide convenient aliases without "Nitro" prefix
Expand Down
8 changes: 4 additions & 4 deletions nitro_validator/core/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ class NitroValidationRule:
Subclass this to add a new rule. Set two class attributes and
implement :meth:`validate`:

* ``name`` the string used in pipe-delimited rule syntax
* ``name`` - the string used in pipe-delimited rule syntax
(e.g. ``"required"``, ``"min"``).
* ``message`` the default error template. Placeholders ``{field}``,
* ``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``
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
Expand Down Expand Up @@ -58,7 +58,7 @@ 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
``*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.
Expand Down
16 changes: 16 additions & 0 deletions nitro_validator/core/rule_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,19 @@ def all(self) -> Dict[str, Type[NitroValidationRule]]:
def clear(self) -> None:
"""Remove every registered rule. Useful for building a registry from scratch."""
self._rules.clear()

def copy(self) -> "NitroRuleRegistry":
"""Return a new registry with the same rule registrations.

The returned registry is independent: registering or unregistering
rules on it does not affect the original. Rule classes themselves
are not copied (registries store references to classes, not
instances).

Returns:
A new :class:`NitroRuleRegistry` populated with the same
name-to-class mappings as this one.
"""
new_registry = NitroRuleRegistry()
new_registry._rules = self._rules.copy()
return new_registry
44 changes: 25 additions & 19 deletions nitro_validator/core/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class NitroValidator:
either returns the validated subset or raises
:class:`NitroValidationError`.

Rules can be expressed two ways interchangeably within the same
Rules can be expressed two ways, interchangeably within the same
call:

* Pipe-delimited strings: ``"required|email"``, ``"min:18"``,
Expand Down Expand Up @@ -84,7 +84,7 @@ def validate(
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
rules with a value of ``None``. Most rules pass on ``None``, so
pair with ``required`` to enforce presence.

Args:
Expand Down Expand Up @@ -118,44 +118,44 @@ def validate(
for field, field_rules in rules.items():
value = data.get(field)

# Parse rules if they're a string
if isinstance(field_rules, str):
parsed_rules = self._parse_rules(field_rules)
else:
parsed_rules = field_rules

# Validate each rule
for rule in parsed_rules:
# Create rule instance if it's a string
if isinstance(rule, str):
rule_instance = self._create_rule_from_string(rule)
elif isinstance(rule, NitroValidationRule):
rule_instance = rule
else:
continue

# Check if field has custom messages
override_message: Optional[str] = None
field_messages = messages.get(field, {})
if isinstance(field_messages, str):
# Single message for all rules
rule_instance.custom_message = field_messages
override_message = field_messages
elif isinstance(field_messages, dict):
# Specific message for this rule
rule_name = rule_instance.name or rule_instance.__class__.__name__.lower()
if rule_name in field_messages:
rule_instance.custom_message = field_messages[rule_name]
override_message = field_messages[rule_name]

# Run validation
if not rule_instance.validate(field, value, data):
if field not in self.errors:
self.errors[field] = []
self.errors[field].append(rule_instance.get_message(field))
if override_message is not None:
prior = rule_instance.custom_message
rule_instance.custom_message = override_message
try:
self.errors[field].append(rule_instance.get_message(field))
finally:
rule_instance.custom_message = prior
else:
self.errors[field].append(rule_instance.get_message(field))

# Add to validated data if no errors
if field not in self.errors:
self.validated_data[field] = value

# Raise exception if there are errors
if self.errors:
raise NitroValidationError(self.errors)

Expand Down Expand Up @@ -237,20 +237,26 @@ def _create_rule_from_string(self, rule_string: str) -> NitroValidationRule:
NitroValidationRule instance

Raises:
NitroRuleNotFoundError: If the rule is not found
NitroRuleNotFoundError: If the rule is not found.
NitroInvalidRuleError: If the string parses as a known rule
but with the wrong number of arguments.

Note:
The pipe and comma are used to split rules and arguments
respectively, so they cannot appear in rule arguments. Rules
that need patterns containing ``|`` or ``,`` (notably
``regex``) must be passed as a rule instance instead of a
string, e.g. ``RegexRule(r"^(a|b)$")``.
"""
# Parse rule name and arguments
if ":" in rule_string:
rule_name, args_string = rule_string.split(":", 1)
args = [arg.strip() for arg in args_string.split(",")]
else:
rule_name = rule_string
args = []

# Get rule class from registry
rule_class = self.registry.get(rule_name)

# Create and return rule instance
return rule_class(*args)

@classmethod
Expand All @@ -264,7 +270,7 @@ def make(
"""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
validator instance afterwards, e.g. to read
:attr:`validated_data`.

Args:
Expand Down
2 changes: 1 addition & 1 deletion nitro_validator/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def register_builtin_rules(registry: NitroRuleRegistry) -> None:

The default registry returned by :class:`NitroValidator()
<nitro_validator.NitroValidator>` is already populated with these
rules call this only when you are building a custom registry
rules; call this only when you are building a custom registry
from scratch and want the built-ins available under their standard
names.

Expand Down
Loading
Loading