From 8f14bf9a33cac93d990b82a1d69c1c56f37ca15e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Jun 2026 19:36:53 +0000 Subject: [PATCH 1/2] Add no_item_validation decorator argument to parse and parse_cls Provide for skipping container item validation across all parameters by passing no_item_validation=True to the parse / parse_cls decorators, e.g. @parse(no_item_validation=True). This has the same effect as including the NO_ITEM_VALIDATION constant to every parameter's Annotated metadata. The decorators now support both bare (@parse) and called (@parse(...)) forms. Resolves #6. https://claude.ai/code/session_01Tu9LcAL1F5xrFpJSsEqLc3 --- docs/tutorials/tutorial.ipynb | 6 +- src/valimp/valimp.py | 86 +++++++++++++++++++++++++--- tests/test_valimp.py | 103 ++++++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 12 deletions(-) diff --git a/docs/tutorials/tutorial.ipynb b/docs/tutorials/tutorial.ipynb index c2fa3f7..bfd6504 100644 --- a/docs/tutorials/tutorial.ipynb +++ b/docs/tutorials/tutorial.ipynb @@ -1007,9 +1007,7 @@ "cell_type": "markdown", "id": "280d5776-ec00-4d8a-944a-7ab6e2957555", "metadata": {}, - "source": [ - "Including `valimp.NO_ITEM_VALIDATION` to an annotation's metadata will result in the contained items not being validated at any level of nesting." - ] + "source": "Including `valimp.NO_ITEM_VALIDATION` to an annotation's metadata will result in the contained items not being validated at any level of nesting.\n\nItem validation can be skipped for **all** parameters by passing `no_item_validation=True` to the decorator, for example `@parse(no_item_validation=True)` (or `@parse_cls(no_item_validation=True)`). This has the same effect as including `valimp.NO_ITEM_VALIDATION` to the metadata of every parameter's annotation." }, { "cell_type": "code", @@ -2454,4 +2452,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/src/valimp/valimp.py b/src/valimp/valimp.py index 83567e1..feed165 100644 --- a/src/valimp/valimp.py +++ b/src/valimp/valimp.py @@ -212,6 +212,21 @@ class ADataclass: Including NO_ITEM_VALIDATION to the annotation's metadata will result in the contained items not being validated at any level of nesting. +Item validation can be skipped for ALL parameters by passing the +`no_item_validation` argument to the decorator as True. For example, the +following will validate that 'a', 'b' and 'c' receive a list, dict and +tuple respectively, but will not validate the type of any of the items: + @parse(no_item_validation=True) + def f( + a: list[str], + b: dict[str, int], + c: tuple[str, ...], + ): + ... +This is equivalent to including the NO_ITEM_VALIDATION constant to the +metadata of every parameter's annotation. The `no_item_validation` +argument can likewise be passed to `parse_cls`. + Coercion and Parsing -------------------- An input can be coerced to a specific type by annotating the parameter with @@ -436,6 +451,7 @@ def validates_against_hint( # noqa: C901, PLR0911, PLR0912 hint: type[Any] | typing._Final, annotated: typing._AnnotatedAlias | None, rtrn_error: bool = True, # noqa: FBT001, FBT002 + no_item_validation: bool = False, # noqa: FBT001, FBT002 ) -> tuple[bool, ValueError | TypeError | None]: """Query if object conforms with type hint. @@ -459,6 +475,11 @@ def validates_against_hint( # noqa: C901, PLR0911, PLR0912 validating hints comprising of nested hints, for example Union[str, List[str], Dict[str, Union[int, float]]. + no_item_validation + Skip validation of the type of items in any container, at all + levels of nesting. Has the same effect as including the + `NO_ITEM_VALIDATION` constant to the `annotated` metadata. + Returns ------- tuple[bool, ValueError | TypeError | None] @@ -479,7 +500,11 @@ def validates_against_hint( # noqa: C901, PLR0911, PLR0912 hint_args = typing.get_args(hint) for hint_ in hint_args: validated, _ = validates_against_hint( - obj, hint_, annotated, rtrn_error=False + obj, + hint_, + annotated, + rtrn_error=False, + no_item_validation=no_item_validation, ) if validated: return VALIDATED @@ -563,8 +588,10 @@ def validates_against_hint( # noqa: C901, PLR0911, PLR0912 # Validation of container ITEMS - if not obj or ( - annotated is not None and NO_ITEM_VALIDATION in annotated.__metadata__ + if ( + not obj + or no_item_validation + or (annotated is not None and NO_ITEM_VALIDATION in annotated.__metadata__) ): return VALIDATED @@ -651,6 +678,7 @@ def validates_against_hint( # noqa: C901, PLR0911, PLR0912 def validate_against_hints( kwargs: dict[str, Any], hints: dict[str, type[Any] | typing._Final], + no_item_validation: bool = False, # noqa: FBT001, FBT002 ) -> dict[str, ValueError | TypeError]: """Validate inputs against hints. @@ -664,6 +692,10 @@ def validate_against_hints( All parameter inputs to be validated. Key as parameter name, value as object received by parameter. + no_item_validation + Skip validation of the type of items in any container, at all + levels of nesting, for all parameters. + Returns ------- errors @@ -681,7 +713,9 @@ def validate_against_hints( else: annotated = None - validated, error = validates_against_hint(obj, hint, annotated) + validated, error = validates_against_hint( + obj, hint, annotated, no_item_validation=no_item_validation + ) if not validated: if error is None: raise AssertionError @@ -965,11 +999,28 @@ def get_unreceived_kwargs( return {k: v for k, v in spec.kwonlydefaults.items() if k not in names_received} -def parse(f) -> collections.abc.Callable: # noqa: C901 +def parse( # noqa: C901 + f=None, + *, + no_item_validation: bool = False, +) -> collections.abc.Callable: """Decorator to validate and parse user inputs. + Can be used directly as `@parse` or called with arguments as + `@parse(...)`. + + Parameters + ---------- + no_item_validation + Skip validation of the type of items in any container, at all + levels of nesting, for all parameters. This has the same effect + as including the `NO_ITEM_VALIDATION` constant to the + `typing.Annotated` metadata of every parameter, defaults to False. + See valimp module doc (valimp.__doc__). """ # noqa: D401 + if f is None: + return functools.partial(parse, no_item_validation=no_item_validation) spec = inspect.getfullargspec(f) hints = typing.get_type_hints(f, include_extras=True) hints = fix_hints_for_none_default(hints, spec) @@ -1029,7 +1080,9 @@ def wrapped_f(*args, **kwargs) -> Any: # noqa: C901, PLR0912 if (k in all_param_names + extra_kwargs) or k.startswith(name_extra_args) } - ann_errors = validate_against_hints(params_as_kwargs, hints_) + ann_errors = validate_against_hints( + params_as_kwargs, hints_, no_item_validation=no_item_validation + ) if sig_errors or ann_errors: raise InputsError(f.__name__, sig_errors, ann_errors) @@ -1080,9 +1133,16 @@ def wrapped_f(*args, **kwargs) -> Any: # noqa: C901, PLR0912 return wrapped_f -def parse_cls(cls): +def parse_cls( + cls=None, + *, + no_item_validation: bool = False, +): """Decorate a class to parse the constructor's arguments. + Can be used directly as `@parse_cls` or called with arguments as + `@parse_cls(...)`. + Can be used to verify input to a `dataclasses.dataclass`. In the following example the a and b parameters will be parsed in the same manner as if the __init__ method were decorated with @parse: @@ -1100,6 +1160,16 @@ class ADataCls: NB The @parse_cls decorator must be placed above the @dataclass decorator. + + Parameters + ---------- + no_item_validation + Skip validation of the type of items in any container, at all + levels of nesting, for all parameters. This has the same effect + as including the `NO_ITEM_VALIDATION` constant to the + `typing.Annotated` metadata of every parameter, defaults to False. """ - cls.__init__ = parse(cls.__init__) + if cls is None: + return functools.partial(parse_cls, no_item_validation=no_item_validation) + cls.__init__ = parse(cls.__init__, no_item_validation=no_item_validation) return cls diff --git a/tests/test_valimp.py b/tests/test_valimp.py index f033f04..bc3e953 100644 --- a/tests/test_valimp.py +++ b/tests/test_valimp.py @@ -2111,3 +2111,106 @@ def f( regex = re.compile("^" + INVALID_MSG_TYPE + "$") with pytest.raises(m.InputsError, match=regex): f(str, int, float, 3) + + +def test_no_item_validation_decorator_arg(): + """Verify `no_item_validation` decorator argument to `parse`.""" + + @m.parse(no_item_validation=True) + def f( + a: list[int], + b: dict[str, int], + c: tuple[int, ...], + d: set[int], + e: Union[str, list[int]], + f_: abc.Sequence[int], + g: abc.Mapping[str, int], + # nested containers, validation skipped at all levels of nesting + h: tuple[list[set[int]], ...], + ) -> tuple: + return a, b, c, d, e, f_, g, h + + # all items invalid, but not validated as no_item_validation is True + rtrn = f( + ["one", "two"], + {0: "val0", 1: "val1"}, + ("one", "two"), + {"one", "two"}, + ["one", "two"], + ["one", "two"], + {0: "val0"}, + ([{"one"}, "not a set"],), + ) + assert rtrn == ( + ["one", "two"], + {0: "val0", 1: "val1"}, + ("one", "two"), + {"one", "two"}, + ["one", "two"], + ["one", "two"], + {0: "val0"}, + ([{"one"}, "not a set"],), + ) + + # verify containers themselves are still validated + msg = ( + "The following inputs to 'f' do not conform with the corresponding type" + " annotation:\n\na\n\tTakes type " + ) + with pytest.raises(m.InputsError, match=re.escape(msg)): + f( + "not a list", + {0: "val0"}, + ("one",), + {"one"}, + ["one"], + ["one"], + {0: "val0"}, + ([{"one"}],), + ) + + +def test_no_item_validation_decorator_arg_default(): + """Verify default behaviour with `no_item_validation` as False. + + Items should be validated as normal when the decorator is called + with arguments but `no_item_validation` left as the default. + """ + + @m.parse() + def f(a: list[int]) -> list[int]: + return a + + assert f([1, 2]) == [1, 2] + + msg = ( + "The following inputs to 'f' do not conform with the corresponding type" + " annotation:\n\na\n\tTakes type containing items" + ) + with pytest.raises(m.InputsError, match=re.escape(msg)): + f(["one"]) + + +def test_no_item_validation_parse_cls_arg(): + """Verify `no_item_validation` decorator argument to `parse_cls`.""" + + @m.parse_cls(no_item_validation=True) + @dataclasses.dataclass + class DataCls: + """A decorated dataclass that skips item validation.""" + + a: list[int] + b: dict[str, int] + + # items invalid, but not validated as no_item_validation is True + obj = DataCls(["one", "two"], {0: "val0"}) + assert obj.a == ["one", "two"] + assert obj.b == {0: "val0"} + + # verify containers themselves are still validated + msg = ( + "The following inputs to '__init__' do not conform with the corresponding" + " type annotation:\n\na\n\tTakes type " + ) + with pytest.raises(m.InputsError, match=re.escape(msg)): + DataCls("not a list", {0: "val0"}) From 76e701a97e5a44630629372540c2dfd6e2905da3 Mon Sep 17 00:00:00 2001 From: Marcus Read Date: Tue, 2 Jun 2026 23:03:21 +0100 Subject: [PATCH 2/2] Manual revisions --- docs/tutorials/tutorial.ipynb | 307 ++++++++++++++++++++-------------- src/valimp/valimp.py | 23 +-- tests/test_valimp.py | 54 +++--- 3 files changed, 220 insertions(+), 164 deletions(-) diff --git a/docs/tutorials/tutorial.ipynb b/docs/tutorials/tutorial.ipynb index bfd6504..2b5db68 100644 --- a/docs/tutorials/tutorial.ipynb +++ b/docs/tutorials/tutorial.ipynb @@ -21,8 +21,8 @@ " - [`dict`](#dict-and-collections.abc.Mapping) and [`collections.abc.Mapping`](#dict-and-collections.abc.Mapping)\n", " - [`tuple`](#tuple)\n", " - [`set`](#set)\n", - " - [`valimp.NO_ITEM_VALIDATION`](#valimp.NO_ITEM_VALIDATION)\n", " - [Nested containers](#Nested-containers)\n", + " - [`valimp.NO_ITEM_VALIDATION`](#valimp.NO_ITEM_VALIDATION)\n", " - [`collections.abc.Callable`](#collections.abc.Callable)\n", " - [`type`](#type)\n", " - [`typing.Literal`](#typing.Literal)\n", @@ -810,84 +810,7 @@ }, { "cell_type": "markdown", - "id": "04095eb6-dcfc-4c0c-a11b-6a7fb7e75142", - "metadata": {}, - "source": [ - "#### `valimp.NO_ITEM_VALIDATION`\n", - "\n", - "To **not** validate a container's items just include the `valimp.NO_ITEM_VALIDATION` constant in an annotation's metadata.\n", - "\n", - "Annotation metadata is defined by simply wrapping the annotation in `typing.Annotated`. The first argument of the `typing.Annotated` subscription takes the wrapped annotation, all further arguments are consumed as annotation metadata.\n", - "\n", - "Notice how in the following example the parameters with `valimp.NO_ITEM_VALIDATION` are not included in the error message - they are considered valid as the container type is correct and the contained items are not validated." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "b38c3307-6fbe-4fb8-ba75-16a1955cc2a8", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Annotated\n", - "from valimp import NO_ITEM_VALIDATION\n", - "\n", - "@parse\n", - "def pf(\n", - " a: tuple[str, ...],\n", - " a1: Annotated[tuple[str, ...], NO_ITEM_VALIDATION],\n", - " b: dict[str, int],\n", - " b1: Annotated[dict[str, int], NO_ITEM_VALIDATION],\n", - " c: set[int],\n", - " c1: Annotated[set[int], NO_ITEM_VALIDATION],\n", - "):\n", - " return" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4bd89977-3f22-4208-9d70-22415f8d2bd3", - "metadata": {}, - "outputs": [], - "source": [ - "pf(\n", - " a=(\"a\", 0), # INVALID as contains int\n", - " a1=(\"a\", 0), # ...INVALID for same reason but will not raise error\n", - " b={\"bkey\": \"bval\"}, # INVALID as has value as str\n", - " b1={\"bkey\": \"bval\"}, # ...INVALID for same reason but will not raise error\n", - " c={0, 1, 2.2}, # INVALID as contains a float\n", - " c1={0, 1, 2.2}, # ...INVALID for same reason but will not raise error\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "80a18cfd-eb49-41f3-b70d-30e5bf4d6178", - "metadata": {}, - "source": [ - "```\n", - "---------------------------------------------------------------------------\n", - "InputsError Traceback (most recent call last)\n", - "Cell In[24], line 1\n", - "----> 1 pf(\n", - "\n", - "InputsError: The following inputs to 'pf' do not conform with the corresponding type annotation:\n", - "\n", - "a\n", - "\tTakes type containing items that conform with , although the received container contains item '0' of type .\n", - "\n", - "b\n", - "\tTakes type with values that conform to the second argument of , although the received dictionary contains value 'bval' of type .\n", - "\n", - "c\n", - "\tTakes type containing items that conform with , although the received container contains item '2.2' of type .\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "b2f6c8e3-26ca-44be-bd65-c8150dcab8a9", + "id": "28329834-92ef-490c-bf79-0cd56f325c4a", "metadata": {}, "source": [ "#### Nested containers\n", @@ -897,7 +820,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 23, "id": "c5159fb4-bb68-445b-ba45-07509458d305", "metadata": {}, "outputs": [], @@ -920,7 +843,7 @@ }, { "cell_type": "markdown", - "id": "6687cb32-eb6f-4048-aa9d-b7522d9b145d", + "id": "6f6ea8d2-0fb4-4a85-b910-98ff0742d103", "metadata": {}, "source": [ "If the first nested container (the list) contains an invalid item then an error is raised (although the error message could be more insightful)..." @@ -945,7 +868,7 @@ }, { "cell_type": "markdown", - "id": "d988bdb6-653e-4810-a73f-126a30ccfb43", + "id": "5276c3ae-79a9-462c-8aff-67ca79823d83", "metadata": {}, "source": [ "```\n", @@ -963,7 +886,7 @@ }, { "cell_type": "markdown", - "id": "98bf95e1-7f71-4987-b6d1-bb514ba79f7d", + "id": "96ad318c-b120-4021-9c0a-a3155586f562", "metadata": {}, "source": [ "An error is also raised if the list contents are valid although one of the second level of nested containers (the sets) contains an invalid item..." @@ -987,7 +910,7 @@ }, { "cell_type": "markdown", - "id": "27868c1f-ea54-4180-9cc8-57038df44264", + "id": "3c739116-b69d-4267-bce6-f9aad9ed0df9", "metadata": {}, "source": [ "```\n", @@ -1003,11 +926,90 @@ "```" ] }, + { + "cell_type": "markdown", + "id": "04095eb6-dcfc-4c0c-a11b-6a7fb7e75142", + "metadata": {}, + "source": [ + "#### `valimp.NO_ITEM_VALIDATION`\n", + "\n", + "To **not** validate a container's items just include the `valimp.NO_ITEM_VALIDATION` constant in an annotation's metadata.\n", + "\n", + "Annotation metadata is defined by simply wrapping the annotation in `typing.Annotated`. The first argument of the `typing.Annotated` subscription takes the wrapped annotation, all further arguments are consumed as annotation metadata.\n", + "\n", + "Notice how in the following example the parameters with `valimp.NO_ITEM_VALIDATION` are not included in the error message - they are considered valid as the container type is correct and the contained items are not validated." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "b38c3307-6fbe-4fb8-ba75-16a1955cc2a8", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Annotated\n", + "from valimp import NO_ITEM_VALIDATION\n", + "\n", + "@parse\n", + "def pf(\n", + " a: tuple[str, ...],\n", + " a1: Annotated[tuple[str, ...], NO_ITEM_VALIDATION],\n", + " b: dict[str, int],\n", + " b1: Annotated[dict[str, int], NO_ITEM_VALIDATION],\n", + " c: set[int],\n", + " c1: Annotated[set[int], NO_ITEM_VALIDATION],\n", + "):\n", + " return" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4bd89977-3f22-4208-9d70-22415f8d2bd3", + "metadata": {}, + "outputs": [], + "source": [ + "pf(\n", + " a=(\"a\", 0), # INVALID as contains int\n", + " a1=(\"a\", 0), # ...INVALID for same reason but will not raise error\n", + " b={\"bkey\": \"bval\"}, # INVALID as has value as str\n", + " b1={\"bkey\": \"bval\"}, # ...INVALID for same reason but will not raise error\n", + " c={0, 1, 2.2}, # INVALID as contains a float\n", + " c1={0, 1, 2.2}, # ...INVALID for same reason but will not raise error\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "80a18cfd-eb49-41f3-b70d-30e5bf4d6178", + "metadata": {}, + "source": [ + "```\n", + "---------------------------------------------------------------------------\n", + "InputsError Traceback (most recent call last)\n", + "Cell In[24], line 1\n", + "----> 1 pf(\n", + "\n", + "InputsError: The following inputs to 'pf' do not conform with the corresponding type annotation:\n", + "\n", + "a\n", + "\tTakes type containing items that conform with , although the received container contains item '0' of type .\n", + "\n", + "b\n", + "\tTakes type with values that conform to the second argument of , although the received dictionary contains value 'bval' of type .\n", + "\n", + "c\n", + "\tTakes type containing items that conform with , although the received container contains item '2.2' of type .\n", + "```" + ] + }, { "cell_type": "markdown", "id": "280d5776-ec00-4d8a-944a-7ab6e2957555", "metadata": {}, - "source": "Including `valimp.NO_ITEM_VALIDATION` to an annotation's metadata will result in the contained items not being validated at any level of nesting.\n\nItem validation can be skipped for **all** parameters by passing `no_item_validation=True` to the decorator, for example `@parse(no_item_validation=True)` (or `@parse_cls(no_item_validation=True)`). This has the same effect as including `valimp.NO_ITEM_VALIDATION` to the metadata of every parameter's annotation." + "source": [ + "Including `valimp.NO_ITEM_VALIDATION` to an annotation's metadata will result in the contained items not being validated at any level of nesting." + ] }, { "cell_type": "code", @@ -1086,7 +1088,60 @@ "id": "af40fc35-dce8-40aa-8fd5-39e9d75bcbfc", "metadata": {}, "source": [ - "NB It isn't currently possible to ignore validation only from a specific level of nesting - PRs welcome!" + "Item validation can be skipped for **all** parameters by passing `no_item_validation=True` to the decorator. This has the same effect as including `valimp.NO_ITEM_VALIDATION` to the metadata of every parameter's annotation. Here we have the same function as at the start of this section but with `no_item_validation=True` passed to the decorator and without any parameter defining the `NO_ITEM_VALIDATION` in `Annotated` metadata." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "3d52b51b-b906-4b2d-9ec6-f77367a72f47", + "metadata": {}, + "outputs": [], + "source": [ + "@parse(no_item_validation=True)\n", + "def pf(\n", + " a: tuple[str, ...],\n", + " a1: tuple[str, ...],\n", + " b: dict[str, int],\n", + " b1: dict[str, int],\n", + " c: set[int],\n", + " c1: set[int],\n", + "):\n", + " return" + ] + }, + { + "cell_type": "markdown", + "id": "7be1d72e-e401-4cd1-9a1d-2fd5d4944d72", + "metadata": {}, + "source": [ + "Calling it with the same arguments as before, i.e. with all arguments containing invalid items, we see that it's now validated:" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "be52d5ad-4ec6-4c2f-8722-59faca22bbbe", + "metadata": {}, + "outputs": [], + "source": [ + "pf(\n", + " a=(\"a\", 0), # INVALID as contains int\n", + " a1=(\"a\", 0), # ...INVALID for same reason but will not raise error\n", + " b={\"bkey\": \"bval\"}, # INVALID as has value as str\n", + " b1={\"bkey\": \"bval\"}, # ...INVALID for same reason but will not raise error\n", + " c={0, 1, 2.2}, # INVALID as contains a float\n", + " c1={0, 1, 2.2}, # ...INVALID for same reason but will not raise error\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7ec38c47-8cb5-45a0-97e4-fd43b03872fd", + "metadata": {}, + "source": [ + "Note that the `no_item_validation` argument can likewise be passed to `parse_cls`." ] }, { @@ -1103,7 +1158,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 33, "id": "44232c3f-b399-492b-a66c-1ab4ce983cc2", "metadata": {}, "outputs": [], @@ -1177,7 +1232,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 35, "id": "0fb179b8", "metadata": {}, "outputs": [ @@ -1187,7 +1242,7 @@ "(dict, __main__.Dog, str)" ] }, - "execution_count": 33, + "execution_count": 35, "metadata": {}, "output_type": "execute_result" } @@ -1269,7 +1324,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 37, "id": "f9041474-ec0a-4488-8e80-ddcbbcbe6069", "metadata": {}, "outputs": [], @@ -1352,7 +1407,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 39, "id": "c6162215-5749-4be5-a8d2-683ac53748f0", "metadata": {}, "outputs": [], @@ -1381,7 +1436,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 40, "id": "12a4aca1-005f-469c-b895-e8473ac50e86", "metadata": {}, "outputs": [ @@ -1391,7 +1446,7 @@ "'spam'" ] }, - "execution_count": 38, + "execution_count": 40, "metadata": {}, "output_type": "execute_result" } @@ -1412,7 +1467,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 41, "id": "bb10d385-cf0e-412f-ae74-2a98aa6aea95", "metadata": {}, "outputs": [], @@ -1468,7 +1523,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 43, "id": "50470b59-5421-4036-8c93-d80624bac11d", "metadata": {}, "outputs": [], @@ -1485,7 +1540,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 44, "id": "a56eb1f1-14d7-41d7-8b40-23c54f73b88a", "metadata": {}, "outputs": [ @@ -1495,7 +1550,7 @@ "(3, (4, 5.0, 'six'), False, {'kw_extra0': 'extra0', 'kw_extra1': [1, 1]})" ] }, - "execution_count": 42, + "execution_count": 44, "metadata": {}, "output_type": "execute_result" } @@ -1514,7 +1569,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 45, "id": "e9dead85-ba39-425f-9858-9565230e2a1b", "metadata": {}, "outputs": [], @@ -1531,7 +1586,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 46, "id": "6880feb1-5e76-430a-9bee-24ad32ff85bb", "metadata": {}, "outputs": [ @@ -1541,7 +1596,7 @@ "(3, (4, 5.0), False, {'kw_b': True, 'kw_c': False})" ] }, - "execution_count": 44, + "execution_count": 46, "metadata": {}, "output_type": "execute_result" } @@ -1596,7 +1651,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 48, "id": "1eca3fca-eef8-425d-998d-d0deef4fc3ff", "metadata": {}, "outputs": [], @@ -1665,7 +1720,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 50, "id": "eff9ce77-2c87-40ca-81c9-4faa54be0997", "metadata": {}, "outputs": [], @@ -1762,7 +1817,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 53, "id": "aa10cb66-5db5-425a-ba4e-fad8e7e8d3eb", "metadata": {}, "outputs": [], @@ -1783,7 +1838,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 54, "id": "8aba30cd-bf06-4b86-b9fd-958bb226d29f", "metadata": {}, "outputs": [ @@ -1798,7 +1853,7 @@ " 'args': (1.0, 2.0, 3.3, None)}" ] }, - "execution_count": 52, + "execution_count": 54, "metadata": {}, "output_type": "execute_result" } @@ -1872,7 +1927,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 55, "id": "872b9027-3aff-4901-859b-f8b1163b5a54", "metadata": {}, "outputs": [ @@ -1882,7 +1937,7 @@ "{'a': 'input_a', 'b': 'input_b_suffix'}" ] }, - "execution_count": 53, + "execution_count": 55, "metadata": {}, "output_type": "execute_result" } @@ -1913,7 +1968,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 56, "id": "3639b3c6-eb1f-493c-a62c-47b36e460fcb", "metadata": {}, "outputs": [ @@ -1927,7 +1982,7 @@ " 'kwargs': {'kw_xtra': \"I'm kw_xtra_kw_xtra\"}}" ] }, - "execution_count": 54, + "execution_count": 56, "metadata": {}, "output_type": "execute_result" } @@ -2009,7 +2064,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 58, "id": "aa1c9d23-908c-4b07-a5b1-59bc043c6d7c", "metadata": {}, "outputs": [], @@ -2112,7 +2167,7 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 61, "id": "bf0921b4-a961-450e-b719-f7bdea866c66", "metadata": {}, "outputs": [], @@ -2130,7 +2185,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 62, "id": "2e85bfd6-05c9-48c1-ada4-82f684476311", "metadata": {}, "outputs": [ @@ -2140,7 +2195,7 @@ "(10, 10)" ] }, - "execution_count": 60, + "execution_count": 62, "metadata": {}, "output_type": "execute_result" } @@ -2162,7 +2217,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 63, "id": "5e8d4eba-bcf8-4c11-978a-49dcba926250", "metadata": {}, "outputs": [], @@ -2179,7 +2234,7 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 64, "id": "4c50f083-b166-4a62-bb13-5ceec8ddca81", "metadata": {}, "outputs": [ @@ -2189,7 +2244,7 @@ "8" ] }, - "execution_count": 62, + "execution_count": 64, "metadata": {}, "output_type": "execute_result" } @@ -2200,7 +2255,7 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 65, "id": "05f9433c-64af-48b3-bb70-80cbcec9c03d", "metadata": {}, "outputs": [], @@ -2218,7 +2273,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 66, "id": "7f00520f-0c4f-48e1-8f65-fd4cde8c2202", "metadata": {}, "outputs": [], @@ -2297,7 +2352,7 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 68, "id": "0171f026-b3d0-434c-bd19-118a829c07a3", "metadata": {}, "outputs": [], @@ -2327,7 +2382,7 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 69, "id": "b5fc576a-1c65-467c-bbb2-a67b412d3297", "metadata": {}, "outputs": [ @@ -2337,7 +2392,7 @@ "(2.2, None)" ] }, - "execution_count": 67, + "execution_count": 69, "metadata": {}, "output_type": "execute_result" } @@ -2348,7 +2403,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 70, "id": "e05c6d24-60a5-4ec1-9e1b-b98f9f8ee094", "metadata": {}, "outputs": [ @@ -2358,7 +2413,7 @@ "(2.2, 3.3)" ] }, - "execution_count": 68, + "execution_count": 70, "metadata": {}, "output_type": "execute_result" } @@ -2369,7 +2424,7 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": 71, "id": "78cc5f4e-ac44-42bb-ada0-106dd023803f", "metadata": {}, "outputs": [ @@ -2379,7 +2434,7 @@ "(2.2, 3.0)" ] }, - "execution_count": 69, + "execution_count": 71, "metadata": {}, "output_type": "execute_result" } @@ -2452,4 +2507,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/src/valimp/valimp.py b/src/valimp/valimp.py index feed165..743c48e 100644 --- a/src/valimp/valimp.py +++ b/src/valimp/valimp.py @@ -189,6 +189,13 @@ class ADataclass: be validated as being a subclass of one of the union members: f(param: type[Union[int, str]]) +Nested containers +Items in nested containers will by default by validated against the +subscripted annoations. For example the following will validate that the +outer tuple contains any number of lists, that those lists contain only +sets and that the sets contains only int or float. + f(param: tuple[list[set[Union[int, float]]], ...]) + NO_ITEM_VALIDATION Validation of the type of items in a container can be skipped for any parameter by using `typing.Annotated` to define the annotation and @@ -202,20 +209,14 @@ class ADataclass: ] ) -Nested containers -Items in nested containers will by default by validated against the -subscripted annoations. For example the following will validate that the -outer tuple contains any number of lists, that those lists contain only -sets and that the sets contains only int or float. - f(param: tuple[list[set[Union[int, float]]], ...]) - Including NO_ITEM_VALIDATION to the annotation's metadata will result in -the contained items not being validated at any level of nesting. +contained items not being validated at any level of nesting. Item validation can be skipped for ALL parameters by passing the `no_item_validation` argument to the decorator as True. For example, the following will validate that 'a', 'b' and 'c' receive a list, dict and -tuple respectively, but will not validate the type of any of the items: +tuple respectively, but will not validate the type of any of the +contained items: @parse(no_item_validation=True) def f( a: list[str], @@ -1015,7 +1016,7 @@ def parse( # noqa: C901 Skip validation of the type of items in any container, at all levels of nesting, for all parameters. This has the same effect as including the `NO_ITEM_VALIDATION` constant to the - `typing.Annotated` metadata of every parameter, defaults to False. + `typing.Annotated` metadata of every parameter. Defaults to False. See valimp module doc (valimp.__doc__). """ # noqa: D401 @@ -1167,7 +1168,7 @@ class ADataCls: Skip validation of the type of items in any container, at all levels of nesting, for all parameters. This has the same effect as including the `NO_ITEM_VALIDATION` constant to the - `typing.Annotated` metadata of every parameter, defaults to False. + `typing.Annotated` metadata of every parameter. Defaults to False. """ if cls is None: return functools.partial(parse_cls, no_item_validation=no_item_validation) diff --git a/tests/test_valimp.py b/tests/test_valimp.py index bc3e953..8ff5f23 100644 --- a/tests/test_valimp.py +++ b/tests/test_valimp.py @@ -2130,18 +2130,8 @@ def f( ) -> tuple: return a, b, c, d, e, f_, g, h - # all items invalid, but not validated as no_item_validation is True - rtrn = f( - ["one", "two"], - {0: "val0", 1: "val1"}, - ("one", "two"), - {"one", "two"}, - ["one", "two"], - ["one", "two"], - {0: "val0"}, - ([{"one"}, "not a set"],), - ) - assert rtrn == ( + # all items invalid, but not verified as `no_item_validation` is True + args = [ ["one", "two"], {0: "val0", 1: "val1"}, ("one", "two"), @@ -2150,31 +2140,42 @@ def f( ["one", "two"], {0: "val0"}, ([{"one"}, "not a set"],), - ) + ] + rtrn = f(*args) + assert rtrn == tuple(args) # verify containers themselves are still validated + args[0] = "not a list" msg = ( "The following inputs to 'f' do not conform with the corresponding type" " annotation:\n\na\n\tTakes type " ) with pytest.raises(m.InputsError, match=re.escape(msg)): - f( - "not a list", - {0: "val0"}, - ("one",), - {"one"}, - ["one"], - ["one"], - {0: "val0"}, - ([{"one"}],), - ) + f(*args) + + +def test_no_item_validation_decorator_arg_false(): + """Verify behaviour with `no_item_validation` as False.""" + + @m.parse(no_item_validation=False) + def f(a: list[int]) -> list[int]: + return a + + assert f([1, 2]) == [1, 2] + + msg = ( + "The following inputs to 'f' do not conform with the corresponding type" + " annotation:\n\na\n\tTakes type containing items" + ) + with pytest.raises(m.InputsError, match=re.escape(msg)): + f(["one"]) def test_no_item_validation_decorator_arg_default(): - """Verify default behaviour with `no_item_validation` as False. + """Verify default behaviour of `no_item_validation`. Items should be validated as normal when the decorator is called - with arguments but `no_item_validation` left as the default. + without arguments (i.e. as if `no_item_validation=False`). """ @m.parse() @@ -2182,7 +2183,6 @@ def f(a: list[int]) -> list[int]: return a assert f([1, 2]) == [1, 2] - msg = ( "The following inputs to 'f' do not conform with the corresponding type" " annotation:\n\na\n\tTakes type containing items" @@ -2202,7 +2202,7 @@ class DataCls: a: list[int] b: dict[str, int] - # items invalid, but not validated as no_item_validation is True + # items invalid, but not verified as `no_item_validation` is True obj = DataCls(["one", "two"], {0: "val0"}) assert obj.a == ["one", "two"] assert obj.b == {0: "val0"}