Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for *args and **kwargs and also dataclasses #9

Merged
merged 4 commits into from
Feb 19, 2024
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
4 changes: 2 additions & 2 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
# fail it if doesn't conform to black
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: psf/black@stable
with:
options: "--check --verbose"
Expand All @@ -30,7 +30,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/draft-release-notes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ jobs:
update_release_draft:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v5
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18 changes: 9 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.x'

Expand All @@ -32,13 +32,13 @@ jobs:
with:
user: __token__
password: ${{ secrets.PYPI_TEST_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
repository-url: https://test.pypi.org/legacy/

- name: Install from testpypi and import
run: |
i=0
while [ $i -lt 12 ] && [ "${{ github.ref_name }}" != $(pip index versions -i https://test.pypi.org/simple --pre valimp | cut -d'(' -f2 | cut -d')' -f1 | sed 1q) ];\
do echo "waiting for package to appear in test index, i is $i, sleeping 5s"; sleep 5s; echo "woken up"; ((i++)); echo "next i is $i"; done
sleep 5
while [ "${{ github.ref_name }}" != $(pip index versions -i https://test.pypi.org/simple --pre valimp | cut -d'(' -f2 | cut -d')' -f1 | sed 1q) ];\
do echo "waiting for package to appear in test index, sleeping 5s"; sleep 5s; echo "woken up"; done
pip install --index-url https://test.pypi.org/simple valimp==${{ github.ref_name }} --no-deps
pip install -r requirements.txt
python -c 'import valimp;print(valimp.__version__)'
Expand All @@ -56,8 +56,8 @@ jobs:

- name: Install and import
run: |
i=0
while [ $i -lt 12 ] && [ "${{ github.ref_name }}" != $(pip index versions -i https://pypi.org/simple --pre valimp | cut -d'(' -f2 | cut -d')' -f1 | sed 1q) ];\
do echo "waiting for package to appear in index, i is $i, sleeping 5s"; sleep 5s; echo "woken up"; ((i++)); echo "next i is $i"; done
sleep 5
while [ "${{ github.ref_name }}" != $(pip index versions -i https://pypi.org/simple --pre valimp | cut -d'(' -f2 | cut -d')' -f1 | sed 1q) ];\
do echo "waiting for package to appear in index, sleeping 5s"; sleep 5s; echo "woken up"; done
pip install --index-url https://pypi.org/simple valimp==${{ github.ref_name }}
python -c 'import valimp;print(valimp.__version__)'
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v4.5.0
hooks:
- id: check-yaml
- repo: https://github.com/psf/black
rev: 23.7.0
rev: 24.2.0
hooks:
- id: black
# It is recommended to specify the latest version of Python
Expand All @@ -13,7 +13,7 @@ repos:
# https://pre-commit.com/#top_level-default_language_version
language_version: python3.11
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
rev: 7.0.0
hooks:
- id: flake8
additional_dependencies: [flake8-docstrings]
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
# usually to register additional checkers.
load-plugins=pylint.extensions.broad_try_clause,
pylint.extensions.confusing_elif,
pylint.extensions.comparetozero,
; pylint.extensions.comparetozero, ; as of Nov 23 fails to load
pylint.extensions.bad_builtin,
pylint.extensions.mccabe,
pylint.extensions.docstyle,
Expand Down
73 changes: 54 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<!-- 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**.
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.

Expand Down Expand Up @@ -37,25 +37,33 @@ def public_function(
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, "g":g, "h":h}
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
a="zero",
b=1.0,
c={"two": 2},
d=3.3, # will be coerced from float to int, i.e. to 3
e="four", # will be parsed to "four_e_zero"
f=5, # will be coerced to str and then parsed to "5_f"
"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)
)
Expand All @@ -68,6 +76,7 @@ returns:
'd': 3,
'e': 'four_e_zero',
'f': '5_f',
'args': (10, 20),
'g': 'keyword_arg_g',
'h': 1.0}
```
Expand Down Expand Up @@ -130,7 +139,30 @@ The following inputs to 'public_function' do not conform with the corresponding
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`
Expand All @@ -147,23 +179,25 @@ Further documentation can be found in the module docstring of [valimp.py](https:
### 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**. For internal validation, consider using a type checker (for example, [mypy](https://github.com/python/mypy)).
> :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 be 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).
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. If that's what you're after then you should definitely be looking there.
[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.

The Pydantic V2 `@validate_call` decorator does provide for validating function input against type hints. However, I think it's [fair to say](https://github.com/pydantic/pydantic/issues/6794) that it's less functional than the `@validate_arguments` decorator of Pydantic V1 (of note, in V2 it's not possible to validate later parameters based on values received by earlier parameters). This loss of functionality, together with finding `@validate_arguments` somewhat clunky to do anything beyond simple type validation, led me to write `valimp`.
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).)

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. If you want to be able to dynamically reference earlier parameters when parsing/validating then definitely have a look at `valimp`.
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:
* variable length arguments (i.e. *args in the function signature).
* variable length keyword arguments (i.e. **kwargs in the function signature).
* precluding positional-only arguments being passed as keyword arguments.
`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:
Expand All @@ -183,8 +217,9 @@ If you only want to validate the type of function inputs then Pydantic V2 `@vali
* `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.

That's it for now, although the library has been built with development in mind and PRs are very much welcome!
The library has been built with development in mind and PRs are very much welcome!

## License

Expand Down
Loading
Loading