valimp


Namevalimp JSON
Version 0.3 PyPI version JSON
download
home_page
SummaryValidate and parse function inputs
upload_time2024-02-19 10:35:05
maintainer
docs_urlNone
authorMarcus Read
requires_python~=3.9
licenseMIT License
keywords validation parsing validate parse coerce input function
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            <!-- NB any links not defined as absolute will not resolve on PyPI page -->
# valimp

<!-- UPDATE BADGE ADDRESSES! -->
[![PyPI](https://img.shields.io/pypi/v/valimp)](https://pypi.org/project/valimp/) ![Python Support](https://img.shields.io/pypi/pyversions/valimp) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

In Python use type hints to validate, parse and coerce inputs to **public functions and dataclasses**. 

This is the sole use of `valimp`. It's a single short module with no depenencies that does one thing and makes it simple to do.

Works like this:
```python
from valimp import parse, Parser, Coerce
from typing import Annotated, Union, Optional, Any

@parse  # add the `valimp.parse`` decorator to a public function or method
def public_function(
    # validate against built-in or custom types
    a: str,
    # support for type unions
    b: Union[int, float],  # or from Python 3.10 `int | float`
    # validate type of container items
    c: dict[str, Union[int, float]],  # dict[str, int | float]
    # coerce input to a specific type
    d: Annotated[
        Union[int, float, str],  # int | float | str
        Coerce(int)
    ],
    # parse input with reference to earlier inputs...
    e: Annotated[
        str,
        Parser(lambda name, obj, params: obj + f"_{name}_{params['a']}")
    ],
    # coerce and parse input...
    f: Annotated[
        Union[str, int],  # str | int
        Coerce(str),
        Parser(lambda name, obj, _: obj + f"_{name}")
    ],
    # support for packing extra arguments if required, can be optionally typed...
    *args: Annotated[
        Union[int, float, str],  # int | float | str
        Coerce(int)
    ],
    # support for optional types
    g: Optional[str],  # str | None
    # define default values dynamically with reference to earlier inputs
    h: Annotated[
        Optional[float],  # float | None
        Parser(lambda _, obj, params: params["b"] if obj is None else obj)
    ] = None,
    # support for packing excess kwargs if required, can be optionally typed...
    # **kwargs: Union[int, float]
) -> dict[str, Any]:
    return {"a":a, "b":b, "c":c, "d":d, "e":e, "f":f, "args",args, "g":g, "h":h}

public_function(
    # NB parameters 'a' through 'f' could be passed positionally
    "zero",  # a
    1.0,  # b
    {"two": 2},  # c
    3.3,  # d, will be coerced from float to int, i.e. to 3
    "four",  # e, will be parsed to "four_e_zero"
    5,  # f, will be coerced to str and then parsed to "5_f"
    "10",  # extra arg, will be coerced to int and packed
    20,  # extra arg, will be packed
    g="keyword_arg_g",
    # h, not passed, will be assigned dynamically as parameter b (i.e. 1.0)
)
```
returns:
```
{'a': 'zero',
 'b': 1.0,
 'c': {'two': 2},
 'd': 3,
 'e': 'four_e_zero',
 'f': '5_f',
 'args': (10, 20),
 'g': 'keyword_arg_g',
 'h': 1.0}
 ```
 And if there are invalid inputs...
```python
public_function(
    a=["not a string"],  # INVALID
    b="not an int or a float",  # INVALID
    c={2: "two"},  # INVALID, key not a str and value not an int or float
    d=3.2, # valid input
    e="valid input",
    f=5.0,  # INVALID, not a str or an int
    g="valid input",
)
```
raises:
```
InputsError: The following inputs to 'public_function' do not conform with the corresponding type annotation:

a
	Takes type <class 'str'> although received '['not a string']' of type <class 'list'>.

b
	Takes input that conforms with <(<class 'int'>, <class 'float'>)> although received 'not an int or a float' of type <class 'str'>.

c
	Takes type <class 'dict'> with keys that conform to the first argument and values that conform to the second argument of <dict[str, typing.Union[int, float]]>, although the received dictionary contains an item with key '2' of type <class 'int'> and value 'two' of type <class 'str'>.

f
	Takes input that conforms with <(<class 'str'>, <class 'int'>)> although received '5.0' of type <class 'float'>.
```
And if the inputs do not match the signature...
```python
public_function(
    "zero",
    "invalid input",  # invalid (not int or float), included in errors
    {"two": 2},
    3.2,
    # no argument passed for required positional arg 'e'
    # no argument passed for required positional arg 'f'
    a="a again",  # passing multiple values for parameter 'a'
    # no argument passed for required keyword arg 'g'
    not_a_kwarg="not a kwarg",  # including an unexpected kwarg
)
```
raises:
```
InputsError: Inputs to 'public_function' do not conform with the function signature:

Got multiple values for argument: 'a'.

Got unexpected keyword argument: 'not_a_kwarg'.

Missing 2 positional arguments: 'e' and 'f'.

Missing 1 keyword-only argument: 'g'.

The following inputs to 'public_function' do not conform with the corresponding type annotation:

b
	Takes input that conforms with <(<class 'int'>, <class 'float'>)> although received 'invalid input' of type <class 'str'>.
```
Use all the same functionality to validate, parse and coerce the fields of a dataclass...
```python
from valimp import parse_cls
import dataclasses

@parse_cls  # place valimp decorator above the dataclass decorator
@dataclasses.dataclass
class ADataclass:
    
    a: str
    b: Annotated[
        Union[str, int],
        Coerce(str),
        Parser(lambda name, obj, params: obj + f" {name} {params['a']}")
    ]

rtrn = ADataclass("I'm a and will appear at the end of b", 33)
dataclasses.asdict(rtrn)
```
output:
```
{'a': "I'm a and will appear at the end of b",
 'b': "33 b I'm a and will appear at the end of b"}
```
## Installation

`$ pip install valimp`

No dependencies!

## Documentation
[tutorial.ipynb](https://github.com/maread99/valimp/blob/master/docs/tutorials/tutorial.ipynb) offers a walk-through of all the functionality.

Further documentation can be found in the module docstring of [valimp.py](https://github.com/maread99/valimp/blob/master/src/valimp/valimp.py).

## Why another validation library!?

### Why even validate input type?
Some may argue that validating the type of public inputs is not pythonic and we can 'duck' out of it and let the errors arise where they may. I'd argue that for the sake of adding a decorator I'd rather raise an intelligible error message than have to respond to an issue asking 'why am I getting this error...'.

> :information_source: `valimp` is only intended for handling inputs to **public functions and dataclasses**. For internal validation, consider using a type checker (for example, [mypy](https://github.com/python/mypy)). 

Also, I like the option of abstracting away all parsing, coercion and validation of public inputs and just receiving the formal parameter as required. For example, public methods in [market-prices](https://github.com/maread99/market_prices) often include a 'date' parameter. I like to offer users the convenience to pass this as either a `str`, a `datetime.date` or a `pandas.Timestamp`, although internally I want it as a `pandas.Timestamp`. I can do this with Valimp by simply including `Coerce(pandas.Timestamp)` to the metadata of the type annotation of each 'date' parameter. I also need to validate that the input is timezone-naive and does indeed represent a date rather than a time. I can do this by defining a single `valimp.Parser` and similarly including it to the annotation metadata of the 'date' parameters. Everything's abstracted away. With a little understanding of type annotations the user can see what's going on by simple inspection of the function's signature (as included within the standard help).

### Why wouldn't I just use Pydantic?
[Pydantic](https://github.com/pydantic/pydantic) is orientated towards the validation of inputs to dataclasses. Whilst the Valimp `@parse_cls` decorator does this well for non-complex cases, if you're looking to do more then Pydantic is the place to go.

As for validating public function input, in the early releases of Pydantic V2 the `@validate_call` decorator failed to provide for validating later parameters based on values received by earlier parameters (a [regression](https://github.com/pydantic/pydantic/issues/6794) from the Pydantic V1 `@validate_arguments` decorator). This loss of functionality, together with finding Pydantic somewhat clunky to do anything beyond simple type validation, is what led me to write `valimp`. (I believe functionality to validate later parameters based on values receive by earlier parameters may have since been restored in Pydantic V2, see the [issue](https://github.com/pydantic/pydantic/issues/6794).)

In short, if you only want to validate the type of function inputs then Pydantic V2 `@validate_call` will do the trick. If you're after additional validation, parsing or coercion then chances are you'll find `valimp` to be a simpler option.

## Limitations and Development

`valimp` does NOT currently support:
  - Positional-only arguments. Any '/' in the signature (to define
  positional-only arguments) will be ignored. Consequently valimp DOES
  allow intended positional-only arguments to be passed as keyword
  arguments.
  - Validation of subscripted types in `collections.abc.Callable` (although Valimp will verify that the passed value is callable).

`valimp` currently supports:
* use of the following type annotations:
    * built-in classes, for example `int`, `str`, `list`, `dict` etc
    * custom classes
    * `collections.abc.Sequence`
    * `collections.abc.Mapping`
    * typing.Any
    * typing.Literal
    * typing.Union ( `|` from 3.10 )
    * typing.Optional ( `<cls> | None` from 3.10)
    * collections.abc.Callable, although validation of subscripted types is **not** supported
* validation of container items for the following generic classes:
    * `list`
    * `dict`
    * `tuple`
    * `set`
    * `collections.abc.Sequence`
    * `collections.abc.Mapping`
* packing and optionally coercing, parsing and validating packed objects, i.e. objects received to, for example, *args and **kwargs.

The library has been built with development in mind and PRs are very much welcome!

## License

[MIT License][license]


[license]: https://github.com/maread99/valimp/blob/master/LICENSE.txt

            

Raw data

            {
    "_id": null,
    "home_page": "",
    "name": "valimp",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "~=3.9",
    "maintainer_email": "",
    "keywords": "validation,parsing,validate,parse,coerce,input,function",
    "author": "Marcus Read",
    "author_email": "marcusaread.prog@proton.me",
    "download_url": "https://files.pythonhosted.org/packages/5a/ba/964b245dd0c02d0c97f3cdb95facf81d29e74c3d2c426d8a2375a5d62a53/valimp-0.3.tar.gz",
    "platform": null,
    "description": "<!-- NB any links not defined as absolute will not resolve on PyPI page -->\n# valimp\n\n<!-- UPDATE BADGE ADDRESSES! -->\n[![PyPI](https://img.shields.io/pypi/v/valimp)](https://pypi.org/project/valimp/) ![Python Support](https://img.shields.io/pypi/pyversions/valimp) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n\nIn Python use type hints to validate, parse and coerce inputs to **public functions and dataclasses**. \n\nThis is the sole use of `valimp`. It's a single short module with no depenencies that does one thing and makes it simple to do.\n\nWorks like this:\n```python\nfrom valimp import parse, Parser, Coerce\nfrom typing import Annotated, Union, Optional, Any\n\n@parse  # add the `valimp.parse`` decorator to a public function or method\ndef public_function(\n    # validate against built-in or custom types\n    a: str,\n    # support for type unions\n    b: Union[int, float],  # or from Python 3.10 `int | float`\n    # validate type of container items\n    c: dict[str, Union[int, float]],  # dict[str, int | float]\n    # coerce input to a specific type\n    d: Annotated[\n        Union[int, float, str],  # int | float | str\n        Coerce(int)\n    ],\n    # parse input with reference to earlier inputs...\n    e: Annotated[\n        str,\n        Parser(lambda name, obj, params: obj + f\"_{name}_{params['a']}\")\n    ],\n    # coerce and parse input...\n    f: Annotated[\n        Union[str, int],  # str | int\n        Coerce(str),\n        Parser(lambda name, obj, _: obj + f\"_{name}\")\n    ],\n    # support for packing extra arguments if required, can be optionally typed...\n    *args: Annotated[\n        Union[int, float, str],  # int | float | str\n        Coerce(int)\n    ],\n    # support for optional types\n    g: Optional[str],  # str | None\n    # define default values dynamically with reference to earlier inputs\n    h: Annotated[\n        Optional[float],  # float | None\n        Parser(lambda _, obj, params: params[\"b\"] if obj is None else obj)\n    ] = None,\n    # support for packing excess kwargs if required, can be optionally typed...\n    # **kwargs: Union[int, float]\n) -> dict[str, Any]:\n    return {\"a\":a, \"b\":b, \"c\":c, \"d\":d, \"e\":e, \"f\":f, \"args\",args, \"g\":g, \"h\":h}\n\npublic_function(\n    # NB parameters 'a' through 'f' could be passed positionally\n    \"zero\",  # a\n    1.0,  # b\n    {\"two\": 2},  # c\n    3.3,  # d, will be coerced from float to int, i.e. to 3\n    \"four\",  # e, will be parsed to \"four_e_zero\"\n    5,  # f, will be coerced to str and then parsed to \"5_f\"\n    \"10\",  # extra arg, will be coerced to int and packed\n    20,  # extra arg, will be packed\n    g=\"keyword_arg_g\",\n    # h, not passed, will be assigned dynamically as parameter b (i.e. 1.0)\n)\n```\nreturns:\n```\n{'a': 'zero',\n 'b': 1.0,\n 'c': {'two': 2},\n 'd': 3,\n 'e': 'four_e_zero',\n 'f': '5_f',\n 'args': (10, 20),\n 'g': 'keyword_arg_g',\n 'h': 1.0}\n ```\n And if there are invalid inputs...\n```python\npublic_function(\n    a=[\"not a string\"],  # INVALID\n    b=\"not an int or a float\",  # INVALID\n    c={2: \"two\"},  # INVALID, key not a str and value not an int or float\n    d=3.2, # valid input\n    e=\"valid input\",\n    f=5.0,  # INVALID, not a str or an int\n    g=\"valid input\",\n)\n```\nraises:\n```\nInputsError: The following inputs to 'public_function' do not conform with the corresponding type annotation:\n\na\n\tTakes type <class 'str'> although received '['not a string']' of type <class 'list'>.\n\nb\n\tTakes input that conforms with <(<class 'int'>, <class 'float'>)> although received 'not an int or a float' of type <class 'str'>.\n\nc\n\tTakes type <class 'dict'> with keys that conform to the first argument and values that conform to the second argument of <dict[str, typing.Union[int, float]]>, although the received dictionary contains an item with key '2' of type <class 'int'> and value 'two' of type <class 'str'>.\n\nf\n\tTakes input that conforms with <(<class 'str'>, <class 'int'>)> although received '5.0' of type <class 'float'>.\n```\nAnd if the inputs do not match the signature...\n```python\npublic_function(\n    \"zero\",\n    \"invalid input\",  # invalid (not int or float), included in errors\n    {\"two\": 2},\n    3.2,\n    # no argument passed for required positional arg 'e'\n    # no argument passed for required positional arg 'f'\n    a=\"a again\",  # passing multiple values for parameter 'a'\n    # no argument passed for required keyword arg 'g'\n    not_a_kwarg=\"not a kwarg\",  # including an unexpected kwarg\n)\n```\nraises:\n```\nInputsError: Inputs to 'public_function' do not conform with the function signature:\n\nGot multiple values for argument: 'a'.\n\nGot unexpected keyword argument: 'not_a_kwarg'.\n\nMissing 2 positional arguments: 'e' and 'f'.\n\nMissing 1 keyword-only argument: 'g'.\n\nThe following inputs to 'public_function' do not conform with the corresponding type annotation:\n\nb\n\tTakes input that conforms with <(<class 'int'>, <class 'float'>)> although received 'invalid input' of type <class 'str'>.\n```\nUse all the same functionality to validate, parse and coerce the fields of a dataclass...\n```python\nfrom valimp import parse_cls\nimport dataclasses\n\n@parse_cls  # place valimp decorator above the dataclass decorator\n@dataclasses.dataclass\nclass ADataclass:\n    \n    a: str\n    b: Annotated[\n        Union[str, int],\n        Coerce(str),\n        Parser(lambda name, obj, params: obj + f\" {name} {params['a']}\")\n    ]\n\nrtrn = ADataclass(\"I'm a and will appear at the end of b\", 33)\ndataclasses.asdict(rtrn)\n```\noutput:\n```\n{'a': \"I'm a and will appear at the end of b\",\n 'b': \"33 b I'm a and will appear at the end of b\"}\n```\n## Installation\n\n`$ pip install valimp`\n\nNo dependencies!\n\n## Documentation\n[tutorial.ipynb](https://github.com/maread99/valimp/blob/master/docs/tutorials/tutorial.ipynb) offers a walk-through of all the functionality.\n\nFurther documentation can be found in the module docstring of [valimp.py](https://github.com/maread99/valimp/blob/master/src/valimp/valimp.py).\n\n## Why another validation library!?\n\n### Why even validate input type?\nSome may argue that validating the type of public inputs is not pythonic and we can 'duck' out of it and let the errors arise where they may. I'd argue that for the sake of adding a decorator I'd rather raise an intelligible error message than have to respond to an issue asking 'why am I getting this error...'.\n\n> :information_source: `valimp` is only intended for handling inputs to **public functions and dataclasses**. For internal validation, consider using a type checker (for example, [mypy](https://github.com/python/mypy)). \n\nAlso, I like the option of abstracting away all parsing, coercion and validation of public inputs and just receiving the formal parameter as required. For example, public methods in [market-prices](https://github.com/maread99/market_prices) often include a 'date' parameter. I like to offer users the convenience to pass this as either a `str`, a `datetime.date` or a `pandas.Timestamp`, although internally I want it as a `pandas.Timestamp`. I can do this with Valimp by simply including `Coerce(pandas.Timestamp)` to the metadata of the type annotation of each 'date' parameter. I also need to validate that the input is timezone-naive and does indeed represent a date rather than a time. I can do this by defining a single `valimp.Parser` and similarly including it to the annotation metadata of the 'date' parameters. Everything's abstracted away. With a little understanding of type annotations the user can see what's going on by simple inspection of the function's signature (as included within the standard help).\n\n### Why wouldn't I just use Pydantic?\n[Pydantic](https://github.com/pydantic/pydantic) is orientated towards the validation of inputs to dataclasses. Whilst the Valimp `@parse_cls` decorator does this well for non-complex cases, if you're looking to do more then Pydantic is the place to go.\n\nAs for validating public function input, in the early releases of Pydantic V2 the `@validate_call` decorator failed to provide for validating later parameters based on values received by earlier parameters (a [regression](https://github.com/pydantic/pydantic/issues/6794) from the Pydantic V1 `@validate_arguments` decorator). This loss of functionality, together with finding Pydantic somewhat clunky to do anything beyond simple type validation, is what led me to write `valimp`. (I believe functionality to validate later parameters based on values receive by earlier parameters may have since been restored in Pydantic V2, see the [issue](https://github.com/pydantic/pydantic/issues/6794).)\n\nIn short, if you only want to validate the type of function inputs then Pydantic V2 `@validate_call` will do the trick. If you're after additional validation, parsing or coercion then chances are you'll find `valimp` to be a simpler option.\n\n## Limitations and Development\n\n`valimp` does NOT currently support:\n  - Positional-only arguments. Any '/' in the signature (to define\n  positional-only arguments) will be ignored. Consequently valimp DOES\n  allow intended positional-only arguments to be passed as keyword\n  arguments.\n  - Validation of subscripted types in `collections.abc.Callable` (although Valimp will verify that the passed value is callable).\n\n`valimp` currently supports:\n* use of the following type annotations:\n    * built-in classes, for example `int`, `str`, `list`, `dict` etc\n    * custom classes\n    * `collections.abc.Sequence`\n    * `collections.abc.Mapping`\n    * typing.Any\n    * typing.Literal\n    * typing.Union ( `|` from 3.10 )\n    * typing.Optional ( `<cls> | None` from 3.10)\n    * collections.abc.Callable, although validation of subscripted types is **not** supported\n* validation of container items for the following generic classes:\n    * `list`\n    * `dict`\n    * `tuple`\n    * `set`\n    * `collections.abc.Sequence`\n    * `collections.abc.Mapping`\n* packing and optionally coercing, parsing and validating packed objects, i.e. objects received to, for example, *args and **kwargs.\n\nThe library has been built with development in mind and PRs are very much welcome!\n\n## License\n\n[MIT License][license]\n\n\n[license]: https://github.com/maread99/valimp/blob/master/LICENSE.txt\n",
    "bugtrack_url": null,
    "license": "MIT License",
    "summary": "Validate and parse function inputs",
    "version": "0.3",
    "project_urls": {
        "Issue Tracker": "https://github.com/maread99/valimp/issues",
        "Source Code": "https://github.com/maread99/valimp",
        "documentation": "https://github.com/maread99/valimp",
        "homepage": "https://github.com/maread99/valimp"
    },
    "split_keywords": [
        "validation",
        "parsing",
        "validate",
        "parse",
        "coerce",
        "input",
        "function"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "9f5d9647cc7c3ad0aea98411d63d622bbe4af0322d07d3be0468fd82bdff0c89",
                "md5": "4202c6025cdc73093a7cd006e3c464df",
                "sha256": "fcb316d473458de5e2472979368900bfb3e681830943da075798621f9a6b653d"
            },
            "downloads": -1,
            "filename": "valimp-0.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "4202c6025cdc73093a7cd006e3c464df",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "~=3.9",
            "size": 15684,
            "upload_time": "2024-02-19T10:35:04",
            "upload_time_iso_8601": "2024-02-19T10:35:04.284124Z",
            "url": "https://files.pythonhosted.org/packages/9f/5d/9647cc7c3ad0aea98411d63d622bbe4af0322d07d3be0468fd82bdff0c89/valimp-0.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "5aba964b245dd0c02d0c97f3cdb95facf81d29e74c3d2c426d8a2375a5d62a53",
                "md5": "d1a451c47d686806745cc38d38901b97",
                "sha256": "69db2dc6fb0e58a562b117e772203ac9c3e1da9eee3b65280652edeff46fd6c8"
            },
            "downloads": -1,
            "filename": "valimp-0.3.tar.gz",
            "has_sig": false,
            "md5_digest": "d1a451c47d686806745cc38d38901b97",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "~=3.9",
            "size": 38810,
            "upload_time": "2024-02-19T10:35:05",
            "upload_time_iso_8601": "2024-02-19T10:35:05.929691Z",
            "url": "https://files.pythonhosted.org/packages/5a/ba/964b245dd0c02d0c97f3cdb95facf81d29e74c3d2c426d8a2375a5d62a53/valimp-0.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-02-19 10:35:05",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "maread99",
    "github_project": "valimp",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "requirements": [],
    "lcname": "valimp"
}
        
Elapsed time: 0.42383s