validate-call-safe


Namevalidate-call-safe JSON
Version 0.3.5 PyPI version JSON
download
home_pageNone
SummarySafe, non-error-raising, alternative to Pydantic validate_call decorator
upload_time2024-09-05 23:30:27
maintainerNone
docs_urlNone
authorNone
requires_python>=3.10
licenseMIT
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # validate-call-safe

`validate_call_safe` is a safe, non-error-raising alternative to Pydantic's `validate_call` decorator.
It allows you to validate function arguments while gracefully handling validation errors through an error model,
inspired by effects handlers, returning them as structured data models instead of raising exceptions.

This therefore means that side effects ('erroring') are transformed into return types.
The return type annotation of a decorated function is modified accordingly as the `Union` of the
existing return type with the provided error model type.

## Features

- Validates function arguments using Pydantic's existing `validate_call` decorator
- Returns a custom error model instead of raising exceptions when validation fails
- Configurable error information, including tracebacks
- Compatible with Pydantic v2, tested back to version 2.0.1
- Optional model config and return value validation, as in the original Pydantic `@validate_call` decorator
- Option to validate function body execution (`validate_body`)
- Option to specify additional exceptions to capture when validating body execution (`extra_exceptions`)

## Installation

```bash
pip install validate-call-safe
```

## Usage

### Basic Usage

The simplest possible usage is as a direct alternative to `@validate_call`:

```py
from validate_call_safe import validate_call_safe

def foo(a: int) -> None:
    return a

value = foo(a="bar")  # ErrorModel(error_type='ValidationError', ...)
```

Instead of raising the `ValidationError`, it's captured in a Pydantic model,
specifically an instance of [`ErrorModel`][EM]. Its fields are:

- `error_type`
- `error_details`
- `error_str`
- `error_repr`
- `error_tb`

[EM]: https://github.com/lmmx/validate-call-safe/blob/master/src/validate_call_safe/errors/model.py

### Decorator Forms

`validate_call_safe` offers flexibility in specifying the error model:

1. No brackets:
   ```python
   @validate_call_safe
   def int_noop(a: int) -> int:
       return a
   ```

2. Empty brackets:
   ```python
   @validate_call_safe()
   def int_noop(a: int) -> int:
       return a
   ```

3. Custom error model (or a `Union` of them):
   ```python
   @validate_call_safe(CustomErrorModel)
   def int_noop(a: int) -> int:
       return a
   ```

4. With validation parameters:
   ```python
   @validate_call_safe(validate_return=True)
   def int_noop(a: int) -> int:
       return a
   ```

5. With reporting parameters:
   ```python
   @validate_call_safe(report=True, reporter=logger.info)
   def int_noop(a: int) -> int:
       return a
   ```

### Custom Error Models

To get more concise error model objects, you might want to override the default `ErrorModel` class
with your own, and just include the fields you want.

For example:

```python
from pydantic import BaseModel
from validate_call_safe import validate_call_safe, ErrorDetails

class MyErrorModel(BaseModel):
    error_type: str
    error_details: list[ErrorDetails]

@validate_call_safe(MyErrorModel)
def int_noop(a: int) -> int:
    return a

success = int_noop(a=1)  # 1
failure = int_noop(a="A")  # MyErrorModel(error_type='ValidationError', ...)
```

#### Unions of Error Models

As well as a single custom decorator `error_model`, you can specify multiple in a Union type.
These cannot be directly parsed into, so first the error is parsed into a regular `ErrorModel`
then dumped into a `TypeAdapter` parameterised by the Union type provided as the custom error model.

For example, you could select particular `ValidationError` kinds based on `error_details`,
or more simply just distinguish a model of an `AttributeError` vs. `ValidationError` like this:

```py
class NoSuchAttribute(BaseModel):
    error_type: Literal["AttributeError"]


class Invalid(BaseModel):
    error_type: Literal["ValidationError"]
```

**Caution**: if your union is not total [comprehensive over the types of error that you are allowing to raise
through setting `extra_exceptions`], say if you set `validate_body` on a function that asserts, but
then specify the error models above that only capture `error_type` of ValidationError and AttributeError,
then the `AssertionError` will slip through the union TypeAdapter and raise!

For safeguarding, include the default `ErrorModel` in a custom union, as this will always be
trivially validated from the initial `ErrorModel` instance.

See [`examples/error_unions`][eu] for sample code.

[eu]: https://github.com/lmmx/validate-call-safe/tree/master/examples/error_unions

### Return Value Validation

You can enable return value validation using the `validate_return` parameter,
which is passed along to the original Pydantic `@validate_call` decorator flag of the same name:

```python
@validate_call_safe(validate_return=True)
def botched_return(a: int) -> int:
    return "foo"  # This will cause a validation error

result = botched_return(a=1)  # ErrorModel(error_type='ValidationError', ...)
```

### Function Body Validation

To capture exceptions that occur within the function body, use the `validate_body` parameter:

```python
@validate_call_safe(validate_body=True)
def failing_function(name: str):
    raise ValueError(f"Invalid name: {name}")

result = failing_function("John")  # ErrorModel(error_type='ValueError', ...)
```

### Validation reporting

Input kw/args and (when used with `validate_return=True`) return value can be 'reported'
by passing `report=True` and optionally a custom `reporter` (default: `print`)

```python
@validate_call_safe(report=True)
def int_noop(a: int) -> int:
    return a

result = int_noop(1)  # prints "int_noop_in_out_validated -> int: 1"
```

### Capturing Additional Exceptions

You can specify additional exceptions to capture using the `extra_exceptions` parameter:

```python
@validate_call_safe(validate_body=True, extra_exceptions=(NameError, TypeError))
def risky_function(a: int):
    if a == 1:
        raise NameError("Name not found")
    elif a == 2:
        raise TypeError("Type mismatch")
    return a

result1 = risky_function(1)  # ErrorModel(error_type='NameError', ...)
result2 = risky_function(2)  # ErrorModel(error_type='TypeError', ...)
result3 = risky_function(3)  # 3
```

The `extra_exception` default is `Exception` (enough for most user-level exceptions,
but will not stop `sys.exit` calls for which you'd need to capture `BaseException`).

Specifying it is useful to narrow the handled exception types, as is good practice
with regular `try`/`except` exception handling.

## Comparison with `validate_call`

With `validate_call_safe` you don't have to catch the expected `ValidationError` from Pydantic's `validate_call`:

```python
from pydantic import validate_call

@validate_call
def unsafe_int_noop(a: int) -> int:
    return a

try:
    unsafe_int_noop(a="A")
except ValidationError as e:
    print(f"Error: {e}")
else:
    ...  # Regular business logic here
```

Using `validate_call_safe`:

```py
from validate_call_safe import validate_call_safe, ErrorModel

@validate_call_safe:
def safe_int_noop(a: int) -> int:
    return a

result = safe_int_noop(a="A")
match result:
    case ErrorModel():
        print(f"Error: {result.error_repr}")
    case int():
        ...  # Regular business logic here
```

- These both do the same thing and have the same number of lines
- In the safe form, you get structured error objects to work with (including tracebacks)
- You can trivially extend the safety level to more exception types with `validate_body`
- The side effects of the safe form may be easier to reason about for you (I think they are)

## Extensions/ideas

- Multiple model types for different error types with tagged union on the `error_type` field name?

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "validate-call-safe",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": null,
    "author": null,
    "author_email": "Louis Maddox <louismmx@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/ef/e9/b012deaa951f59a7596424f9853a2e5dfce6aa1a4b4d1d72cc69afe0046f/validate_call_safe-0.3.5.tar.gz",
    "platform": null,
    "description": "# validate-call-safe\n\n`validate_call_safe` is a safe, non-error-raising alternative to Pydantic's `validate_call` decorator.\nIt allows you to validate function arguments while gracefully handling validation errors through an error model,\ninspired by effects handlers, returning them as structured data models instead of raising exceptions.\n\nThis therefore means that side effects ('erroring') are transformed into return types.\nThe return type annotation of a decorated function is modified accordingly as the `Union` of the\nexisting return type with the provided error model type.\n\n## Features\n\n- Validates function arguments using Pydantic's existing `validate_call` decorator\n- Returns a custom error model instead of raising exceptions when validation fails\n- Configurable error information, including tracebacks\n- Compatible with Pydantic v2, tested back to version 2.0.1\n- Optional model config and return value validation, as in the original Pydantic `@validate_call` decorator\n- Option to validate function body execution (`validate_body`)\n- Option to specify additional exceptions to capture when validating body execution (`extra_exceptions`)\n\n## Installation\n\n```bash\npip install validate-call-safe\n```\n\n## Usage\n\n### Basic Usage\n\nThe simplest possible usage is as a direct alternative to `@validate_call`:\n\n```py\nfrom validate_call_safe import validate_call_safe\n\ndef foo(a: int) -> None:\n    return a\n\nvalue = foo(a=\"bar\")  # ErrorModel(error_type='ValidationError', ...)\n```\n\nInstead of raising the `ValidationError`, it's captured in a Pydantic model,\nspecifically an instance of [`ErrorModel`][EM]. Its fields are:\n\n- `error_type`\n- `error_details`\n- `error_str`\n- `error_repr`\n- `error_tb`\n\n[EM]: https://github.com/lmmx/validate-call-safe/blob/master/src/validate_call_safe/errors/model.py\n\n### Decorator Forms\n\n`validate_call_safe` offers flexibility in specifying the error model:\n\n1. No brackets:\n   ```python\n   @validate_call_safe\n   def int_noop(a: int) -> int:\n       return a\n   ```\n\n2. Empty brackets:\n   ```python\n   @validate_call_safe()\n   def int_noop(a: int) -> int:\n       return a\n   ```\n\n3. Custom error model (or a `Union` of them):\n   ```python\n   @validate_call_safe(CustomErrorModel)\n   def int_noop(a: int) -> int:\n       return a\n   ```\n\n4. With validation parameters:\n   ```python\n   @validate_call_safe(validate_return=True)\n   def int_noop(a: int) -> int:\n       return a\n   ```\n\n5. With reporting parameters:\n   ```python\n   @validate_call_safe(report=True, reporter=logger.info)\n   def int_noop(a: int) -> int:\n       return a\n   ```\n\n### Custom Error Models\n\nTo get more concise error model objects, you might want to override the default `ErrorModel` class\nwith your own, and just include the fields you want.\n\nFor example:\n\n```python\nfrom pydantic import BaseModel\nfrom validate_call_safe import validate_call_safe, ErrorDetails\n\nclass MyErrorModel(BaseModel):\n    error_type: str\n    error_details: list[ErrorDetails]\n\n@validate_call_safe(MyErrorModel)\ndef int_noop(a: int) -> int:\n    return a\n\nsuccess = int_noop(a=1)  # 1\nfailure = int_noop(a=\"A\")  # MyErrorModel(error_type='ValidationError', ...)\n```\n\n#### Unions of Error Models\n\nAs well as a single custom decorator `error_model`, you can specify multiple in a Union type.\nThese cannot be directly parsed into, so first the error is parsed into a regular `ErrorModel`\nthen dumped into a `TypeAdapter` parameterised by the Union type provided as the custom error model.\n\nFor example, you could select particular `ValidationError` kinds based on `error_details`,\nor more simply just distinguish a model of an `AttributeError` vs. `ValidationError` like this:\n\n```py\nclass NoSuchAttribute(BaseModel):\n    error_type: Literal[\"AttributeError\"]\n\n\nclass Invalid(BaseModel):\n    error_type: Literal[\"ValidationError\"]\n```\n\n**Caution**: if your union is not total [comprehensive over the types of error that you are allowing to raise\nthrough setting `extra_exceptions`], say if you set `validate_body` on a function that asserts, but\nthen specify the error models above that only capture `error_type` of ValidationError and AttributeError,\nthen the `AssertionError` will slip through the union TypeAdapter and raise!\n\nFor safeguarding, include the default `ErrorModel` in a custom union, as this will always be\ntrivially validated from the initial `ErrorModel` instance.\n\nSee [`examples/error_unions`][eu] for sample code.\n\n[eu]: https://github.com/lmmx/validate-call-safe/tree/master/examples/error_unions\n\n### Return Value Validation\n\nYou can enable return value validation using the `validate_return` parameter,\nwhich is passed along to the original Pydantic `@validate_call` decorator flag of the same name:\n\n```python\n@validate_call_safe(validate_return=True)\ndef botched_return(a: int) -> int:\n    return \"foo\"  # This will cause a validation error\n\nresult = botched_return(a=1)  # ErrorModel(error_type='ValidationError', ...)\n```\n\n### Function Body Validation\n\nTo capture exceptions that occur within the function body, use the `validate_body` parameter:\n\n```python\n@validate_call_safe(validate_body=True)\ndef failing_function(name: str):\n    raise ValueError(f\"Invalid name: {name}\")\n\nresult = failing_function(\"John\")  # ErrorModel(error_type='ValueError', ...)\n```\n\n### Validation reporting\n\nInput kw/args and (when used with `validate_return=True`) return value can be 'reported'\nby passing `report=True` and optionally a custom `reporter` (default: `print`)\n\n```python\n@validate_call_safe(report=True)\ndef int_noop(a: int) -> int:\n    return a\n\nresult = int_noop(1)  # prints \"int_noop_in_out_validated -> int: 1\"\n```\n\n### Capturing Additional Exceptions\n\nYou can specify additional exceptions to capture using the `extra_exceptions` parameter:\n\n```python\n@validate_call_safe(validate_body=True, extra_exceptions=(NameError, TypeError))\ndef risky_function(a: int):\n    if a == 1:\n        raise NameError(\"Name not found\")\n    elif a == 2:\n        raise TypeError(\"Type mismatch\")\n    return a\n\nresult1 = risky_function(1)  # ErrorModel(error_type='NameError', ...)\nresult2 = risky_function(2)  # ErrorModel(error_type='TypeError', ...)\nresult3 = risky_function(3)  # 3\n```\n\nThe `extra_exception` default is `Exception` (enough for most user-level exceptions,\nbut will not stop `sys.exit` calls for which you'd need to capture `BaseException`).\n\nSpecifying it is useful to narrow the handled exception types, as is good practice\nwith regular `try`/`except` exception handling.\n\n## Comparison with `validate_call`\n\nWith `validate_call_safe` you don't have to catch the expected `ValidationError` from Pydantic's `validate_call`:\n\n```python\nfrom pydantic import validate_call\n\n@validate_call\ndef unsafe_int_noop(a: int) -> int:\n    return a\n\ntry:\n    unsafe_int_noop(a=\"A\")\nexcept ValidationError as e:\n    print(f\"Error: {e}\")\nelse:\n    ...  # Regular business logic here\n```\n\nUsing `validate_call_safe`:\n\n```py\nfrom validate_call_safe import validate_call_safe, ErrorModel\n\n@validate_call_safe:\ndef safe_int_noop(a: int) -> int:\n    return a\n\nresult = safe_int_noop(a=\"A\")\nmatch result:\n    case ErrorModel():\n        print(f\"Error: {result.error_repr}\")\n    case int():\n        ...  # Regular business logic here\n```\n\n- These both do the same thing and have the same number of lines\n- In the safe form, you get structured error objects to work with (including tracebacks)\n- You can trivially extend the safety level to more exception types with `validate_body`\n- The side effects of the safe form may be easier to reason about for you (I think they are)\n\n## Extensions/ideas\n\n- Multiple model types for different error types with tagged union on the `error_type` field name?\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Safe, non-error-raising, alternative to Pydantic validate_call decorator",
    "version": "0.3.5",
    "project_urls": {
        "Homepage": "https://github.com/lmmx/validate-call-safe",
        "Repository": "https://github.com/lmmx/validate-call-safe.git"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "3da74990555712f32966443d65fc87670097073f78aab94683be11e8998ac63e",
                "md5": "d23165ee12633cf9f6b152ded469ec7c",
                "sha256": "dac7696bcee9e8c955f7d219f8932ad988929c6d77ea85f1f5156e03be3ea67d"
            },
            "downloads": -1,
            "filename": "validate_call_safe-0.3.5-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "d23165ee12633cf9f6b152ded469ec7c",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 7707,
            "upload_time": "2024-09-05T23:30:26",
            "upload_time_iso_8601": "2024-09-05T23:30:26.405448Z",
            "url": "https://files.pythonhosted.org/packages/3d/a7/4990555712f32966443d65fc87670097073f78aab94683be11e8998ac63e/validate_call_safe-0.3.5-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "efe9b012deaa951f59a7596424f9853a2e5dfce6aa1a4b4d1d72cc69afe0046f",
                "md5": "36c6aa64cc56a172236518bc4fde50eb",
                "sha256": "fc42098fb079f6934917fadede23d2fb8782bd56f0bef160d61cea0fa2075811"
            },
            "downloads": -1,
            "filename": "validate_call_safe-0.3.5.tar.gz",
            "has_sig": false,
            "md5_digest": "36c6aa64cc56a172236518bc4fde50eb",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 9159,
            "upload_time": "2024-09-05T23:30:27",
            "upload_time_iso_8601": "2024-09-05T23:30:27.986959Z",
            "url": "https://files.pythonhosted.org/packages/ef/e9/b012deaa951f59a7596424f9853a2e5dfce6aa1a4b4d1d72cc69afe0046f/validate_call_safe-0.3.5.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-09-05 23:30:27",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "lmmx",
    "github_project": "validate-call-safe",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "validate-call-safe"
}
        
Elapsed time: 0.53108s