diff --git a/docs/tutorials/tutorial.ipynb b/docs/tutorials/tutorial.ipynb index c2fa3f7..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,6 +926,83 @@ "```" ] }, + { + "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", @@ -1088,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`." ] }, { @@ -1105,7 +1158,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 33, "id": "44232c3f-b399-492b-a66c-1ab4ce983cc2", "metadata": {}, "outputs": [], @@ -1179,7 +1232,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 35, "id": "0fb179b8", "metadata": {}, "outputs": [ @@ -1189,7 +1242,7 @@ "(dict, __main__.Dog, str)" ] }, - "execution_count": 33, + "execution_count": 35, "metadata": {}, "output_type": "execute_result" } @@ -1271,7 +1324,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 37, "id": "f9041474-ec0a-4488-8e80-ddcbbcbe6069", "metadata": {}, "outputs": [], @@ -1354,7 +1407,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 39, "id": "c6162215-5749-4be5-a8d2-683ac53748f0", "metadata": {}, "outputs": [], @@ -1383,7 +1436,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 40, "id": "12a4aca1-005f-469c-b895-e8473ac50e86", "metadata": {}, "outputs": [ @@ -1393,7 +1446,7 @@ "'spam'" ] }, - "execution_count": 38, + "execution_count": 40, "metadata": {}, "output_type": "execute_result" } @@ -1414,7 +1467,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 41, "id": "bb10d385-cf0e-412f-ae74-2a98aa6aea95", "metadata": {}, "outputs": [], @@ -1470,7 +1523,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 43, "id": "50470b59-5421-4036-8c93-d80624bac11d", "metadata": {}, "outputs": [], @@ -1487,7 +1540,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 44, "id": "a56eb1f1-14d7-41d7-8b40-23c54f73b88a", "metadata": {}, "outputs": [ @@ -1497,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" } @@ -1516,7 +1569,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 45, "id": "e9dead85-ba39-425f-9858-9565230e2a1b", "metadata": {}, "outputs": [], @@ -1533,7 +1586,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 46, "id": "6880feb1-5e76-430a-9bee-24ad32ff85bb", "metadata": {}, "outputs": [ @@ -1543,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" } @@ -1598,7 +1651,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 48, "id": "1eca3fca-eef8-425d-998d-d0deef4fc3ff", "metadata": {}, "outputs": [], @@ -1667,7 +1720,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 50, "id": "eff9ce77-2c87-40ca-81c9-4faa54be0997", "metadata": {}, "outputs": [], @@ -1764,7 +1817,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 53, "id": "aa10cb66-5db5-425a-ba4e-fad8e7e8d3eb", "metadata": {}, "outputs": [], @@ -1785,7 +1838,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 54, "id": "8aba30cd-bf06-4b86-b9fd-958bb226d29f", "metadata": {}, "outputs": [ @@ -1800,7 +1853,7 @@ " 'args': (1.0, 2.0, 3.3, None)}" ] }, - "execution_count": 52, + "execution_count": 54, "metadata": {}, "output_type": "execute_result" } @@ -1874,7 +1927,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 55, "id": "872b9027-3aff-4901-859b-f8b1163b5a54", "metadata": {}, "outputs": [ @@ -1884,7 +1937,7 @@ "{'a': 'input_a', 'b': 'input_b_suffix'}" ] }, - "execution_count": 53, + "execution_count": 55, "metadata": {}, "output_type": "execute_result" } @@ -1915,7 +1968,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 56, "id": "3639b3c6-eb1f-493c-a62c-47b36e460fcb", "metadata": {}, "outputs": [ @@ -1929,7 +1982,7 @@ " 'kwargs': {'kw_xtra': \"I'm kw_xtra_kw_xtra\"}}" ] }, - "execution_count": 54, + "execution_count": 56, "metadata": {}, "output_type": "execute_result" } @@ -2011,7 +2064,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 58, "id": "aa1c9d23-908c-4b07-a5b1-59bc043c6d7c", "metadata": {}, "outputs": [], @@ -2114,7 +2167,7 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 61, "id": "bf0921b4-a961-450e-b719-f7bdea866c66", "metadata": {}, "outputs": [], @@ -2132,7 +2185,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 62, "id": "2e85bfd6-05c9-48c1-ada4-82f684476311", "metadata": {}, "outputs": [ @@ -2142,7 +2195,7 @@ "(10, 10)" ] }, - "execution_count": 60, + "execution_count": 62, "metadata": {}, "output_type": "execute_result" } @@ -2164,7 +2217,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 63, "id": "5e8d4eba-bcf8-4c11-978a-49dcba926250", "metadata": {}, "outputs": [], @@ -2181,7 +2234,7 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 64, "id": "4c50f083-b166-4a62-bb13-5ceec8ddca81", "metadata": {}, "outputs": [ @@ -2191,7 +2244,7 @@ "8" ] }, - "execution_count": 62, + "execution_count": 64, "metadata": {}, "output_type": "execute_result" } @@ -2202,7 +2255,7 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 65, "id": "05f9433c-64af-48b3-bb70-80cbcec9c03d", "metadata": {}, "outputs": [], @@ -2220,7 +2273,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 66, "id": "7f00520f-0c4f-48e1-8f65-fd4cde8c2202", "metadata": {}, "outputs": [], @@ -2299,7 +2352,7 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 68, "id": "0171f026-b3d0-434c-bd19-118a829c07a3", "metadata": {}, "outputs": [], @@ -2329,7 +2382,7 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 69, "id": "b5fc576a-1c65-467c-bbb2-a67b412d3297", "metadata": {}, "outputs": [ @@ -2339,7 +2392,7 @@ "(2.2, None)" ] }, - "execution_count": 67, + "execution_count": 69, "metadata": {}, "output_type": "execute_result" } @@ -2350,7 +2403,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 70, "id": "e05c6d24-60a5-4ec1-9e1b-b98f9f8ee094", "metadata": {}, "outputs": [ @@ -2360,7 +2413,7 @@ "(2.2, 3.3)" ] }, - "execution_count": 68, + "execution_count": 70, "metadata": {}, "output_type": "execute_result" } @@ -2371,7 +2424,7 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": 71, "id": "78cc5f4e-ac44-42bb-ada0-106dd023803f", "metadata": {}, "outputs": [ @@ -2381,7 +2434,7 @@ "(2.2, 3.0)" ] }, - "execution_count": 69, + "execution_count": 71, "metadata": {}, "output_type": "execute_result" } diff --git a/src/valimp/valimp.py b/src/valimp/valimp.py index 83567e1..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,15 +209,24 @@ 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 +contained 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 -------------------- @@ -436,6 +452,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 +476,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 +501,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 +589,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 +679,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 +693,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 +714,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 +1000,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 +1081,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 +1134,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 +1161,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..8ff5f23 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 verified as `no_item_validation` is True + args = [ + ["one", "two"], + {0: "val0", 1: "val1"}, + ("one", "two"), + {"one", "two"}, + ["one", "two"], + ["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(*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 of `no_item_validation`. + + Items should be validated as normal when the decorator is called + without arguments (i.e. as if `no_item_validation=False`). + """ + + @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 verified 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"})