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
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
project = 'validatedata'
copyright = '2026, Edward Kigozi'
author = 'Edward Kigozi'
release = '0.6.0'
release = '0.7.0'

extensions = [
'sphinx.ext.autodoc',
Expand Down
48 changes: 47 additions & 1 deletion docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ The performance advantage of the *validator* function on invalid data comes from
is_str_or_int_list(['a', 1, 'c']) # True


One-off checks with ``V``
--------------------------

When you just need a yes/no answer about a single value — no rule, no
compiled validator — use ``V``.

.. code-block:: python

from validatedata import V

if not V.email(request.form.get('email', '')):
return 'invalid email', 400

V.raise_on_fail(True)
V.int(user_input) # raises TypeError if not an int

See :doc:`v` for the full reference.

----

User registration
-----------------

Expand Down Expand Up @@ -540,4 +560,30 @@ When you have a recurring data shape, define a model once and reuse it.
data = product.to_dict()

# Reconstruct from dict (fast path)
product2 = Product.from_dict(data, validate="check")
product2 = Product.from_dict(data, validate="check")

----

Bridging an existing Pydantic model
-------------------------------------

Already have models defined with Pydantic, msgspec, or dataclasses? Bridge
them into ``FastModel`` instead of rewriting them from scratch.

.. code-block:: python

from pydantic import BaseModel, Field
from validatedata import FastModel

class PyProduct(BaseModel):
name: str = Field(min_length=3, max_length=100)
price: float = Field(ge=0)

FastProduct = FastModel.bridge(PyProduct)

product = FastProduct(name="Widget", price=19.99) # compiled validation
data = product.to_dict() # same serialization as any FastModel

See :doc:`fastmodel` for the full reference, including ``extra_rules`` and
``field_overrides`` for constraints (like ``gt``/``lt`` or ``multiple_of``)
that have no direct equivalent in validatedata's engine.
46 changes: 45 additions & 1 deletion docs/fastmodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -321,4 +321,48 @@ whole model — the same function used internally by `is_valid_data` and the
.. code-block:: python

validate = User.get_validator()
validate({"username": "alice", "email": "alice@example.com"}) # True / False
validate({"username": "alice", "email": "alice@example.com"}) # True / False

----

Bridging from Pydantic, msgspec, or dataclasses
-------------------------------------------------

.. versionadded:: 0.7.0

Already have models defined with another library? ``FastModel.bridge()`` builds
an equivalent `FastModel` subclass from a Pydantic model, msgspec ``Struct``, or
dataclass — carrying over field constraints (``min_length``/``max_length``,
``ge``/``le``, ``pattern``, ``Literal`` choices, defaults) so you get compiled
validation and serialization without rewriting the model.

.. code-block:: python

from pydantic import BaseModel, Field
from validatedata import FastModel

class PyUser(BaseModel):
username: str = Field(min_length=3, max_length=32, pattern=r'^[a-z0-9_]+$')
age: int = Field(ge=18)

FastUser = FastModel.bridge(PyUser)

FastUser(username="alice", age=25) # works
FastUser(username="al", age=25) # raises ValidationError

Constraints with no equivalent in validatedata's engine — ``gt``/``lt`` (strict
bounds), ``multiple_of``, and msgspec's ``tz`` — raise ``ValueError`` at bridge
time rather than being silently dropped or loosened. Supply your own rule for
that field via ``extra_rules`` to bridge it anyway:

.. code-block:: python

FastModel.bridge(
PyUser,
extra_rules={"age": "int|min:18"}, # handle it yourself
field_overrides={"username": Rule(min=5, max=10)}, # or replace a rule entirely
model_check=my_cross_field_check,
)

# Bridge an instance directly — returns a populated FastModel instance
bridged_user = FastModel.bridge(PyUser(username="alice", age=25))
18 changes: 8 additions & 10 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,18 @@ An easier way to validate data in python.
**Seven validation modes – one simple syntax.**

1. **`validator()`** – One word: speed. Ideal for high‑throughput streaming. msgspec, handwritten code, and this function will compete for first place.
2. **`FastModel`** – declarative, typed models with compiled validation, rich error messages, and serialization.
2. **`FastModel`** – declarative, typed models with compiled validation, rich error messages, serialization, and one-line bridging from Pydantic, msgspec, or dataclasses.
3. **`V`** – fast validation using simple inline checks, e.g if V.int(5), V.email("not"). Returns bool by default but user can enable exceptions
4. **`validate_data()`** / **`validate_data_fast()`** – general‑purpose validation with detailed errors, nested structures, and optional mutation.
4. **`@validate`** – decorator for function argument validation.
5. **`@validate_types`** – decorator that uses Python type annotations.
6. **`FastModel`** – declarative, typed models with compiled validation, rich error messages, and serialization.
5. **`@validate`** – decorator for function argument validation.
6. **`@validate_types`** – decorator that uses Python type annotations.
7. **`autovalidate` / `autovalidate_package`** – automatically apply `@validate_types` to entire modules or packages.

Validatedata gives you expressive rules and fits naturally into any Python workflow. It can be used in lightweight scripts, Web APIs, and high‑volume data processing.
Validatedata gives you expressive rules and fits naturally into any Python workflow. It can be used by everything from lightweight scripts to high‑volume data processing.

**New in v0.6:**
- **`FastModel`** – declarative models with compiled validation, cross‑field checks, and zero‑overhead serialization.
- **`validate_data_fast`** – the speed of `validator()` combined with rich error messages. This is an **experimental** fast path that will eventually replace `validate_data` once the API stabilises.
- **`autovalidate` & `autovalidate_package`** – automatically apply `@validate_types` to whole modules or packages.
- **Custom type registration** – add your own type checkers with `register_type` / `unregister_type`.
**New in v0.7:**
- **`FastModel.bridge()`** – turn an existing Pydantic model, msgspec ``Struct``, or dataclass into a `FastModel` subclass in one line, carrying over field constraints (`min_length`, `ge`/`le`, `pattern`, `Literal` choices, and more).
- **`V`** – single-line type checks (`V.int(x)`, `V.email(x)`) for when a full `Rule` or `FastModel` is more than you need.
- **`check_rule`** – validate rule dicts before using them.
- **`VALID_RULE_KEYS`** – introspection of all recognised rule keys.

Expand Down Expand Up @@ -106,6 +103,7 @@ Benchmarks (1 million repetitions)
autovalidate
fast-validator
fastmodel
v
examples

.. toctree::
Expand Down
26 changes: 24 additions & 2 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import.
Seven ways to validate
--------------------

Validatedata offers six entry points, from ultra‑fast boolean checks to automatic
Validatedata offers seven entry points, from ultra‑fast boolean checks to automatic
package‑wide validation.

1. **`validator()`** – fastest, boolean only
Expand Down Expand Up @@ -118,7 +118,29 @@ package‑wide validation.

user = User(name="Alice", email="alice@example.com")

7. **Auto‑validation of modules / packages**
Already have a Pydantic model, msgspec ``Struct``, or dataclass? Bridge it
instead of rewriting it — see :doc:`fastmodel` for details.

.. code-block:: python

FastUser = FastModel.bridge(ExistingPydanticModel)

7. **`V`** – single-line type checks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: python

from validatedata import V

if V.int(5):
print('ok')
if not V.email('not-an-email'):
print('invalid')

See :doc:`v` for the full reference, including ``V.raise_on_fail()`` and
``V.check()``.

8. **Auto‑validation of modules / packages**
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: python
Expand Down
69 changes: 69 additions & 0 deletions docs/v.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
V – Single-Line Type Checks
============================

.. versionadded:: 0.7.0

``V`` answers one question about one value, inline — no ``Rule``, no
``FastModel``, no rule dict.

.. code-block:: python

from validatedata import V

if V.int(5):
...
if not V.email(user_input):
raise ValueError("bad email")

``V`` covers the same base types as ``validator()`` — ``int``, ``str``,
``float``, ``bool``, ``list``, ``dict``, ``tuple``, ``set`` — plus format
checks like ``email``, ``url``, ``uuid``, ``date``, ``ip``, ``phone``,
``slug``, ``semver``, ``color``, ``even``, ``odd``, ``prime``, ``decimal``,
``path``. Each is a plain function on the class — nothing to instantiate or
compile.

----

Raising instead of returning ``False``
----------------------------------------

By default a failed check returns ``False``. Call ``V.raise_on_fail(True)`` to
switch every check to a raising variant that throws ``TypeError`` naming the
expected and actual types:

.. code-block:: python

V.raise_on_fail(True)
V.int("not an int")
# TypeError: expected int, got str

V.raise_on_fail(False) # back to bool-returning
V.int("not an int") # False

This is global to the ``V`` class, not per-call. If you need both behaviors
concurrently, use ``V.check(...)`` with your own ``try``/``except`` instead of
toggling this.

----

Types not predeclared on ``V``
---------------------------------

For a type that isn't one of ``V``'s base-type attributes — one you registered
via :func:`register_type`, or a plain Python/stdlib type — use ``V.check()``:

.. code-block:: python

V.check("datetime", some_dt)
V.check("MyRegisteredType", obj)

``V.check()`` always returns a bool and does not honor ``raise_on_fail()``.

----

What ``V`` deliberately doesn't do
--------------------------------------

No rules, no pipe strings, no ``Rule`` composition. Constraint logic (``min``,
``max``, ``pattern``, ``nullable``, ...) belongs to ``Rule``/``FastModel`` —
``V`` only ever answers "is this value this type," optionally loudly.
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "validatedata"
version = "0.7.0"
version = "0.7.1"
authors = [
{ name="Edward Kigozi", email="eddyk1collab@gmail.com" },
]
Expand All @@ -15,7 +15,6 @@ license = "MIT"
requires-python = ">=3.8"
dependencies = [
"python-dateutil",
"typing_extensions>=4.0.0; python_version<'3.9'",
]
keywords = ["validate", "data", "validation"]
classifiers = [
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pytest
python-dateutil>=2.8.2
typing_extensions>=4.0.0; python_version<'3.9'
# typing_extensions>=4.0.0; python_version<'3.9' # optional. for users who want to use annotated
# on older python versions

zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion validatedata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from . import bridge as _bridge # noqa: F401 (attaches FastModel.bridge)
from .v import V

__version__ = '0.7.0'
__version__ = '0.7.1'

__all__ = [
'validate',
Expand Down
6 changes: 5 additions & 1 deletion validatedata/fastmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@
if sys.version_info >= (3, 9):
from typing import Annotated
else:
from typing_extensions import Annotated
try:
from typing_extensions import Annotated
except ImportError:
# Fallback for users on Python < 3.9 who opt out of the extension
Annotated = type("Annotated", (), {})

# ---------------------------------------------------------------------------
# Codegen flag
Expand Down
Loading