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 ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Each capability is a self-contained domain module that provides:
- **Validation Rules** — semantic rules that validate the notation against authoritative specifications
- **A Contract** — a user-facing configuration object that toggles grammars, excludes rules, and passes parameters

Capabilities are registered with the discovery registry before the first canonicalization call. The registry freezes at the start of each pipeline run, ensuring that the set of available capabilities is stable during execution.
Capabilities are registered with the discovery registry before the first canonicalization call. The registry freezes on the first `canonicalize()` call and remains frozen for later runs, ensuring that the set of available capabilities is stable during execution. The sanctioned bulk form is `paxman.register_all_shipped()`, which registers all ten shipped capabilities in fixed alphabetical order; registration — single or bootstrap — must complete from a single thread before the first `canonicalize()` call, after which reads are safe from any thread.

### Engine

Expand Down
8 changes: 5 additions & 3 deletions QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ pip install paxman
import paxman
from paxman.capabilities.Email.capability import EmailCapability
from paxman.core.domain import Resolution
from paxman.core.discovery import register_capability

# Register the Email capability (once, before first use)
register_capability(EmailCapability())
paxman.register_all_shipped() # once, before first use

# Create a contract and canonicalize
contract = EmailCapability.create_contract()
Expand All @@ -32,6 +30,10 @@ if result.status == Resolution.SUCCESS:
print(result.canonicalized_value) # "user@example.com"
```

To register only what you need, call `paxman.register_capability(EmailCapability())` per capability.

Registration — single or bootstrap — must complete from a single thread before the first `canonicalize()` call; post-freeze reads are safe from any thread.

That's it. Paxman recognized the email in your text, validated it against RFC 5322, and returned the lowercase canonical form.

---
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ pip install paxman
```python
import paxman
from paxman.capabilities import Email
from paxman.core.discovery import register_capability
from paxman.core.domain import Resolution

# Register the Email capability (once, before first use)
register_capability(Email())
paxman.register_all_shipped() # once, before first use

# Create a contract and canonicalize
contract = Email.create_contract()
Expand All @@ -34,6 +32,10 @@ if result.status == Resolution.SUCCESS:
print(result.canonicalized_value) # "user@example.com"
```

To register only what you need, call `paxman.register_capability(Email())` per capability.

**Registration and threading:** Registration — single (`register_capability`) or bootstrap (`paxman.register_all_shipped()`) — must complete from a single thread before the first `canonicalize()` call; the registry then freezes and reads are safe from any thread; registering later raises `CapabilityError`.

---

## What Happens
Expand Down
2 changes: 2 additions & 0 deletions paxman/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from paxman.api.bootstrap import register_all_shipped
from paxman.api.canonicalize import canonicalize
from paxman.core.discovery import register_capability
from paxman.core.errors import CapabilityError
Expand All @@ -7,6 +8,7 @@
"CapabilityError",
"canonicalize",
"register_capability",
"register_all_shipped",
"register_grammar",
"register_rule",
]
62 changes: 62 additions & 0 deletions paxman/api/bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Sanctioned bootstrap: register every shipped capability in one call."""

from __future__ import annotations

from typing import Any

from paxman.capabilities import (
IP,
ISBN,
URL,
Country,
Currency,
Date,
Email,
Money,
Phone,
SIUnit,
)
from paxman.core.capability import Capability
from paxman.core.discovery import get_capability, register_capability
from paxman.core.errors import CapabilityError

# Fixed, documented order (alphabetical by capability registry name) —
# bootstrap is deterministic. D2: literal tuple, no dynamic enumeration.
_SHIPPED: tuple[type[Capability[Any]], ...] = (
Country,
Currency,
Date,
Email,
IP,
ISBN,
Money,
Phone,
SIUnit,
URL,
)


def register_all_shipped() -> tuple[str, ...]:
"""Register every shipped capability not already registered.

Idempotent by name: a capability already registered (including a
caller-registered subclass) is skipped, never overridden. Does not
freeze the registry — freezing still happens on the first
``canonicalize()`` call. Raises ``CapabilityError`` if the registry is
already frozen and anything remains to register.

Threading contract: complete registration — single-calls or this
helper — from a single thread before the first ``canonicalize()``
call; post-freeze reads are safe from any thread.

Returns:
Names newly registered, in call order.
"""
registered: list[str] = []
for cls in _SHIPPED:
try:
get_capability(cls.name)
except CapabilityError:
register_capability(cls())
registered.append(cls.name)
return tuple(registered)
25 changes: 25 additions & 0 deletions tests/e2e/test_bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

import pytest

import paxman
from paxman.capabilities import Email
from paxman.core.discovery import reset_registry
from paxman.core.domain import Resolution


@pytest.fixture(autouse=True)
def _clean_registry():
reset_registry()
yield
reset_registry()


@pytest.mark.e2e
def test_bootstrap_then_canonicalize_round_trip() -> None:
"""register_all_shipped() is a complete bootstrap: pipeline resolves."""
paxman.register_all_shipped()
contract = Email.create_contract()
result = paxman.canonicalize("user@Example.COM", contract)
assert result.status is Resolution.SUCCESS
assert result.canonicalized_value == "user@example.com"
79 changes: 79 additions & 0 deletions tests/unit/test_bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Unit tests for the register_all_shipped bootstrap helper."""

from __future__ import annotations

from collections.abc import Iterator

import pytest

import paxman
from paxman.capabilities import Email
from paxman.core.discovery import (
get_capability,
is_registry_frozen,
register_capability,
reset_registry,
)


@pytest.fixture
def _clean_registry() -> Iterator[None]:
reset_registry()
yield
reset_registry()


@pytest.mark.unit
def test_registers_all_ten_shipped(_clean_registry) -> None:
names = paxman.register_all_shipped()
expected = (
"country",
"currency",
"date",
"email",
"ip",
"isbn",
"money",
"phone",
"si_unit",
"url",
)
assert names == expected
for name in expected:
assert get_capability(name).name == name


@pytest.mark.unit
def test_idempotent_second_call_registers_nothing(_clean_registry) -> None:
paxman.register_all_shipped()
assert paxman.register_all_shipped() == ()


@pytest.mark.unit
def test_preserves_caller_registration(_clean_registry) -> None:
mine = Email()
register_capability(mine)
names = paxman.register_all_shipped()
assert "email" not in names
assert len(names) == 9
assert get_capability("email") is mine


@pytest.mark.unit
def test_does_not_freeze_registry(_clean_registry) -> None:
paxman.register_all_shipped()
assert is_registry_frozen() is False


@pytest.mark.unit
def test_raises_after_freeze(_clean_registry) -> None:
"""Natural freeze: one canonicalize() call freezes the registry; a later
bootstrap with anything left to register must surface the error."""
register_capability(Email())
contract = Email.create_contract()
paxman.canonicalize("user@example.com", contract) # freezes naturally
assert is_registry_frozen() is True
# "url" was never registered, so the helper still has work to do — it
# must raise (via register_capability's frozen check), never swallow.
with pytest.raises(paxman.CapabilityError):
paxman.register_all_shipped()
Loading