diff --git a/b2/_internal/_cli/obj_loads.py b/b2/_internal/_cli/obj_loads.py index a9b771980..de4a71ae1 100644 --- a/b2/_internal/_cli/obj_loads.py +++ b/b2/_internal/_cli/obj_loads.py @@ -12,6 +12,8 @@ import argparse import io import json +import logging +import sys from typing import TypeVar from b2sdk.v2 import get_b2sdk_doc_urls @@ -19,9 +21,16 @@ 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 +logger = logging.getLogger(__name__) + def convert_error_to_human_readable(validation_exc: ValidationError) -> str: buf = io.StringIO() @@ -41,18 +50,33 @@ def describe_type(type_) -> str: T = TypeVar('T') +_UNDEF = object() + 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: - ta = TypeAdapter(expected_type) + expected_type = pydantic.with_config(pydantic.ConfigDict(extra="allow"))(expected_type) try: - val = ta.validate_json(data) - except ValidationError as e: - errors = convert_error_to_human_readable(e) - raise argparse.ArgumentTypeError( - f'Invalid value inputted, expected {describe_type(expected_type)}, got {data!r}, more detail below:\n{errors}' - ) from e - else: + ta = TypeAdapter(expected_type) + except TypeError: + # TypeError: unsupported operand type(s) for |: 'type' and 'NoneType' + # This is thrown on python<3.10 even with eval_type_backport + logger.debug( + f'Failed to create TypeAdapter for {expected_type!r} using pydantic, falling back to json.loads', + exc_info=True + ) + val = _UNDEF + else: + try: + val = ta.validate_json(data) + except ValidationError as e: + errors = convert_error_to_human_readable(e) + raise argparse.ArgumentTypeError( + f'Invalid value inputted, expected {describe_type(expected_type)}, got {data!r}, more detail below:\n{errors}' + ) from e + + if val is _UNDEF: try: val = json.loads(data) except json.JSONDecodeError as e: diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index c7418f3ee..15c9177fe 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -835,8 +835,9 @@ class LifecycleRulesMixin(Described): """ Use `--lifecycle-rule` to set lifecycle rule for the bucket. Multiple rules can be specified by repeating the option. - - `--lifecycle-rules` option is deprecated and cannot be used together with --lifecycle-rule. + All bucket lifecycle rules are set at once, so if you want to add a new rule, + you need to provide all existing rules. + Example: :code:`--lifecycle-rule '{{"daysFromHidingToDeleting": 1, "daysFromUploadingToHiding": null, "fileNamePrefix": "documents/"}}' --lifecycle-rule '{{"daysFromHidingToDeleting": 1, "daysFromUploadingToHiding": 7, "fileNamePrefix": "temporary/"}}'` """ @classmethod diff --git a/changelog.d/+lifecycle_rule_validation.fixed.md b/changelog.d/+lifecycle_rule_validation.fixed.md new file mode 100644 index 000000000..052124957 --- /dev/null +++ b/changelog.d/+lifecycle_rule_validation.fixed.md @@ -0,0 +1 @@ +Fix `--lifecycle-rule` validation on `python<3.10`. diff --git a/changelog.d/432.doc.md b/changelog.d/432.doc.md new file mode 100644 index 000000000..20739bb51 --- /dev/null +++ b/changelog.d/432.doc.md @@ -0,0 +1 @@ +Add `--lifecycle-rule` example to CLI `--help` and documentation. diff --git a/test/unit/_cli/test_obj_loads.py b/test/unit/_cli/test_obj_loads.py index 06bcfcc4e..5f8992da1 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)