From 6ded8853855617096215dea48cd7e671d5ab9152 Mon Sep 17 00:00:00 2001 From: Maciej Urbanski Date: Thu, 13 Jun 2024 10:56:58 +0200 Subject: [PATCH] fix validated_loads not allowing extra fields to be included --- b2/_internal/_cli/obj_loads.py | 9 ++++++++- test/unit/_cli/test_obj_loads.py | 31 ++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/b2/_internal/_cli/obj_loads.py b/b2/_internal/_cli/obj_loads.py index d85f6140..9442ef03 100644 --- a/b2/_internal/_cli/obj_loads.py +++ b/b2/_internal/_cli/obj_loads.py @@ -13,6 +13,7 @@ import io import json import logging +import sys from typing import TypeVar from b2sdk.v2 import get_b2sdk_doc_urls @@ -20,6 +21,11 @@ try: import pydantic from pydantic import TypeAdapter, ValidationError + + if sys.version_info < (3, 10): + raise ImportError('pydantic integration is not supported on python<3.10') + # we could support it partially with help of https://github.com/pydantic/pydantic/issues/7873 + # but that creates yet another edge case, on old version of Python except ImportError: pydantic = None @@ -50,6 +56,7 @@ def describe_type(type_) -> str: def validated_loads(data: str, expected_type: type[T] | None = None) -> T: val = _UNDEF if expected_type is not None and pydantic is not None: + expected_type = pydantic.with_config(pydantic.ConfigDict(extra="allow"))(expected_type) try: ta = TypeAdapter(expected_type) except TypeError: @@ -62,7 +69,7 @@ def validated_loads(data: str, expected_type: type[T] | None = None) -> T: val = _UNDEF else: try: - val = ta.validate_json(data) + val = ta.validate_json(data,) except ValidationError as e: errors = convert_error_to_human_readable(e) raise argparse.ArgumentTypeError( diff --git a/test/unit/_cli/test_obj_loads.py b/test/unit/_cli/test_obj_loads.py index 06bcfcc4..5f8992da 100644 --- a/test/unit/_cli/test_obj_loads.py +++ b/test/unit/_cli/test_obj_loads.py @@ -7,11 +7,18 @@ # License https://www.backblaze.com/using_b2_code.html # ###################################################################### +from __future__ import annotations + import argparse import pytest -from b2._internal._cli.obj_loads import validated_loads +try: + from typing_extensions import TypedDict +except ImportError: + from typing import TypedDict + +from b2._internal._cli.obj_loads import pydantic, validated_loads @pytest.mark.parametrize( @@ -46,3 +53,25 @@ def test_validated_loads(input_, expected_val): def test_validated_loads__invalid_syntax(input_, error_msg): with pytest.raises(argparse.ArgumentTypeError, match=error_msg): validated_loads(input_) + + +@pytest.fixture +def typed_dict_cls(): + class MyTypedDict(TypedDict): + a: int | None + b: str + + return MyTypedDict + + +def test_validated_loads__typed_dict(typed_dict_cls): + input_ = '{"a": 1, "b": "2", "extra": null}' + expected_val = {"a": 1, "b": "2", "extra": None} + assert validated_loads(input_, typed_dict_cls) == expected_val + + +@pytest.mark.skipif(pydantic is None, reason="pydantic is not enabled") +def test_validated_loads__typed_dict_types_validation(typed_dict_cls): + input_ = '{"a": "abc", "b": 2}' + with pytest.raises(argparse.ArgumentTypeError): + validated_loads(input_, typed_dict_cls)