Metadata-Version: 2.4
Name: typekeeper
Version: 0.0.4
Summary: An extensive drop-in argument validator.
Author-email: Parth Mittal <parth@privatepanda.co>
License: MIT License
        
        Copyright (c) 2025 Parth Mittal
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/PrivatePandaCO/typekeeper
Project-URL: Documentation, https://github.com/PrivatePandaCO/typekeeper/blob/main/README.md
Project-URL: Changelog, https://github.com/PrivatePandaCO/typekeeper/blob/main/Changelog.md
Project-URL: Source, https://github.com/PrivatePandaCO/typekeeper
Keywords: typing,validation,runtime,decorator,type-checking
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pytest-cov>=4; extra == "dev"
Requires-Dist: coverage>=7; extra == "dev"
Dynamic: license-file

# typekeeper

Lightweight, dependency-free runtime argument validation for Python via a
single decorator. See [Changelog.md](Changelog.md) for version history.

## Overview

`typekeeper` instruments your functions with comprehensive runtime checks
through one decorator, `@validate_args`. With no code changes beyond the
decorator, you get:

- Type checking against rich `typing` annotations.
- Warnings for mismatched or mutable default values.
- Inclusive numeric-range and length-range constraints via a tiny spec
  string, including nested-path targeting with `:` and wildcards.
- Recursive `_validate()` traversal of nested data structures.
- Precise source locations (file + line) for every warning, both at
  decoration time and call time.
- Global, per-decorator, and contextual on/off switches.

## Features

- **Type validation** — recursively checks the common `typing` forms:
  built-in generics (`list[int]`, `dict[str, float]`), `Optional`,
  `Union` / PEP 604 unions, `Literal`, `Type[T]` / `type[T]`,
  `Annotated`, `Final`, `NewType`, `TypeVar` (constraints and bounds),
  `Callable` (callability **and** positional arity), `TypedDict`
  (structural dict check), `Protocol` (structural attribute check for
  both `@runtime_checkable` and plain protocols), `Self`, and abstract
  base classes such as `Sequence`, `Mapping`, and `Iterable`. Unresolved
  string forward references are treated as unchecked; `ForwardRef`
  objects are resolved against the typing namespace when possible.
- **Default-value checks** — warns when a default does not match its
  annotation, and detects mutable defaults (suppressible per decorator
  via `ignore_defaults=True`).
- **Range & length specs** — pass `constraints`; each spec is
  `name=token1,token2,...` where a token is either a single number `N`
  or an inclusive range `min-max`.
  Whitespace around the inner `-` is tolerated. Use `:` to traverse
  nested mappings and sequences, with integer segments selecting
  sequence elements (negative indices follow normal Python rules).
  Wildcards (`*` or a trailing `:`) match every element under that
  point; later specs override earlier ones for the same target. Missing
  mapping keys yield no matches (no spurious warnings).
- **Recursive `_validate()`** — every argument is walked. Any object
  with a callable `_validate()` is invoked; falsy returns or raised
  exceptions are reported with the path inside the original structure.
  Mappings (including `UserDict` / `MappingProxyType`) are recursed via
  `collections.abc.Mapping`. **Iterators and generators are never
  consumed** by validation.
- **Context-aware warnings** — messages embed both the decoration
  location (`file:line`) and the call site.
- **Global control** — `set_arg_checks(False)` disables decoration
  entirely; `set_stop_on_error(True)` turns every warning into a
  `ValueError`; `suspended_arg_checks()` is a context manager that
  switches checks off inside a `with` block (overhead drops to under
  half a microsecond per call).

## Installation

```bash
pip install typekeeper
```

## Development

```bash
pip install -e ".[dev]"
python -m pytest tests/ --cov=typekeeper
```

The test suite covers 100% of `typekeeper/typekeeper.py` and exercises a
broad slice of `typing` features end-to-end.

## Configuration

| API | Effect |
| --- | --- |
| `set_arg_checks(enabled: bool)` | Turn all argument checking on/off globally. Disabling at decoration time skips wrapping entirely (zero overhead). |
| `set_stop_on_error(stop: bool)` | When `True`, every validation failure raises `ValueError` instead of warning. |
| `suspended_arg_checks()` | Context manager that disables checks within a `with` block (decorator stays in place). |

## API Reference

### `validate_args`

```python
def validate_args(
    _func=None,
    constraints: str | None = None,
    *,
    ignore_defaults: bool = False,
    stop_on_error: bool | None = None,
) -> Callable: ...
```

- **`constraints`** — numeric/length spec string covering both numeric
  ranges and sequence-length ranges (see syntax below).
- **`ignore_defaults`** — skip the mutable-default warning if `True`.
- **`stop_on_error`** — per-decorator override of the global
  `set_stop_on_error` flag; warnings from this decorator raise
  `ValueError` regardless of the global setting.

Constraint syntax — a semicolon-separated list:

```text
name=token1,token2,...
```

Each token is `N` (exact match) or `min-max` (inclusive range).
Nested targets use `:` after the name: `users:tables:headers=100`,
`rows:0:score=0-100`, `data:*=1-5`. Whitespace inside ranges is
tolerated (`1 - 3` parses identically to `1-3`).

Inside path segments, the following backslash escapes are honoured:

| Escape | Meaning |
| --- | --- |
| `\:`  | Literal `:` inside a segment. |
| `\\`  | Literal backslash. |
| `\*`  | Literal `*` segment (no wildcard expansion). |
| `\!`  | Literal empty segment — addresses dict keys that are literally `""` without firing the wildcard branch. |

A bare empty segment (e.g. trailing `:`) or `*` still means *every entry
at this level*; specs whose first segment is empty (`":x=1-3"`) emit a
parse-time warning instead of being silently dropped.

## Examples

Full runnable scripts live in [`examples/`](examples/). Each script
prints its own validation overhead via [`examples/_bench.py`](examples/_bench.py).

| File | Topic |
| --- | --- |
| [`basic.py`](examples/basic.py) | Type + default checks. |
| [`range_specs.py`](examples/range_specs.py) | Numeric and length ranges. |
| [`path_indices.py`](examples/path_indices.py) | Integer index segments (incl. negatives). |
| [`nested_override.py`](examples/nested_override.py) | Wildcards and spec overrides. |
| [`custom_validate.py`](examples/custom_validate.py) | `_validate()` recursion + mutable defaults. |
| [`complex_typing.py`](examples/complex_typing.py) | `Union`, `Literal`, `Callable` (arity), `TypeVar`, `NewType`, `Annotated`. |
| [`typeddicts.py`](examples/typeddicts.py) | Structural TypedDict checks (`total=True` / `False`). |
| [`protocols.py`](examples/protocols.py) | `@runtime_checkable` **and** plain `Protocol` (structural attribute check). |
| [`forward_refs.py`](examples/forward_refs.py) | `from __future__ import annotations` and forward references. |
| [`globals_and_context.py`](examples/globals_and_context.py) | `set_arg_checks`, `set_stop_on_error`, `suspended_arg_checks`. |

```python
# Snapshot of basic.py
from typekeeper import validate_args

@validate_args()
def greet(name: str, times: int = 1) -> str:
    return " ".join([name] * times)

greet("Alice")
greet(42)
#  UserWarning: Arg 'name' to '<function greet ...>' mismatches <class 'str'>;
#  expected <class 'str'>, got <class 'int'>
```

## Performance

Run [`tools/profile.py`](tools/profile.py) to reproduce these numbers.
They are representative of a single CPython 3.14 run on a desktop
x86-64 machine (50,000 iterations per scenario, after a warm-up burst):

| Scenario | Bare (µs/call) | Checked (µs/call) | Suspended (µs/call) | Overhead (µs/call) |
| --- | ---: | ---: | ---: | ---: |
| `simple` (`x: int, y: str`) | 0.08 | 10.9 | 0.19 | 10.8 |
| `nested_generic` (`list[dict[str, int]]`, 3 dicts) | 0.35 | 39.5 | 0.66 | 39.1 |
| `constraints` (numeric + length spec) | 0.07 | 23.0 | 0.18 | 22.9 |
| `path_spec` (`rows:*:score=0-100`, 5 rows) | 0.05 | 60.6 | 0.25 | 60.6 |
| `varargs` (`*values: int`, 6 args) | 0.07 | 20.6 | 0.53 | 20.6 |
| `custom_validate` (8-element list with `_validate`) | 0.05 | 25.3 | 0.21 | 25.2 |

Run it yourself:

```bash
python tools/profile.py --iterations 50000          # scenario benchmark
python tools/profile.py --cprofile                  # cProfile breakdown
python tools/profile.py --iterations 50000 --json results.json
```

Key takeaways:

- The decorator itself (suspended mode) costs **well under 1 µs** per
  call (typically 0.2 µs for fixed-arity signatures).
- Scalar arguments add roughly **10 µs** per call for type checking.
- Per-element checking dominates the cost on collection-shaped
  signatures; expect **~5–10 µs / leaf** in the worst case.
- Path-spec wildcards walk every leaf — prefer indexed paths
  (`rows:0:v=...`) when you only care about a few cells.
- The `_recursive_validate()` walk runs on every argument; if your hot
  path passes large structures and you don't need it, hoist the
  validated call out of the loop or wrap it in
  `with suspended_arg_checks(): ...`.

## Contributing

Contributions are welcome:

1. Fork the repository.
2. Create a feature branch.
3. Run `python -m pytest tests/ --cov=typekeeper` (target stays at 100%).
4. Submit a pull request.

## License

MIT License.

## Author

**Parth Mittal** — <parth@privatepanda.co>
