From 8bb6ba48a570812a6035f5a3d83abe9cf9adbf0f Mon Sep 17 00:00:00 2001 From: Sofia Carreras Date: Fri, 30 Oct 2026 19:08:53 -0300 Subject: [PATCH 1/2] Fix crash in type_with_config for parametrized generics when pydantic is installed `list[X]` is a `types.GenericAlias` and cannot take a `__pydantic_config__` attribute, so every JSON option typed `list[...]` (e.g. `--lifecycle-rules`) raised AttributeError when pydantic is installed. The type is now used unchanged in that case. Co-Authored-By: Claude Sonnet 5.5 --- b2/_internal/_cli/obj_loads.py | 7 ++++++- changelog.d/+pydantic-generic-alias.fixed.md | 1 + test/unit/_cli/test_obj_loads.py | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 changelog.d/+pydantic-generic-alias.fixed.md diff --git a/b2/_internal/_cli/obj_loads.py b/b2/_internal/_cli/obj_loads.py index ea8f0b935..52884c594 100644 --- a/b2/_internal/_cli/obj_loads.py +++ b/b2/_internal/_cli/obj_loads.py @@ -57,7 +57,12 @@ def describe_type(type_) -> str: def type_with_config(type_: type[T], config: pydantic.ConfigDict) -> type[T]: type_ = copy.copy(type_) if not hasattr(type_, '__config__'): - type_.__pydantic_config__ = config + try: + type_.__pydantic_config__ = config + except AttributeError: + # parametrized builtin generics (e.g. `list[LifecycleRule]`) are `types.GenericAlias` objects, + # which do not accept new attributes; use the type as is, pydantic applies its defaults + pass else: type_.__config__ = type_.__config__.copy() type_.__config__.update(config) diff --git a/changelog.d/+pydantic-generic-alias.fixed.md b/changelog.d/+pydantic-generic-alias.fixed.md new file mode 100644 index 000000000..dbc47421f --- /dev/null +++ b/changelog.d/+pydantic-generic-alias.fixed.md @@ -0,0 +1 @@ +Fix `b2 bucket create --lifecycle-rule` (and other JSON options typed as a parametrized list) failing with `AttributeError: 'types.GenericAlias' object has no attribute '__pydantic_config__'` when the `full` extra (pydantic) is installed. diff --git a/test/unit/_cli/test_obj_loads.py b/test/unit/_cli/test_obj_loads.py index f92101452..cc1bd8399 100644 --- a/test/unit/_cli/test_obj_loads.py +++ b/test/unit/_cli/test_obj_loads.py @@ -66,3 +66,17 @@ 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) + + +def test_validated_loads__parametrized_generic(typed_dict_cls): + # `list[...]` is a `types.GenericAlias`, which does not accept new attributes + input_ = '[{"a": 1, "b": "2"}]' + expected_val = [{'a': 1, 'b': '2'}] + assert validated_loads(input_, list[typed_dict_cls]) == expected_val + + +@pytest.mark.skipif(pydantic is None, reason='pydantic is not enabled') +def test_validated_loads__parametrized_generic_types_validation(typed_dict_cls): + input_ = '[{"a": "abc", "b": 2}]' + with pytest.raises(argparse.ArgumentTypeError): + validated_loads(input_, list[typed_dict_cls]) From f3fd6c665d8ee7e7a03e6cfe51d820ce1d71bbed Mon Sep 17 00:00:00 2001 From: Sofia Carreras Date: Fri, 30 Oct 2026 19:13:59 -0300 Subject: [PATCH 2/2] Keep extra keys for parametrized generics, fix changelog option name Co-Authored-By: Claude Sonnet 5.5 --- b2/_internal/_cli/obj_loads.py | 11 ++++++++--- changelog.d/+pydantic-generic-alias.fixed.md | 2 +- test/unit/_cli/test_obj_loads.py | 7 ++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/b2/_internal/_cli/obj_loads.py b/b2/_internal/_cli/obj_loads.py index 52884c594..4a75af0a8 100644 --- a/b2/_internal/_cli/obj_loads.py +++ b/b2/_internal/_cli/obj_loads.py @@ -61,7 +61,8 @@ def type_with_config(type_: type[T], config: pydantic.ConfigDict) -> type[T]: type_.__pydantic_config__ = config except AttributeError: # parametrized builtin generics (e.g. `list[LifecycleRule]`) are `types.GenericAlias` objects, - # which do not accept new attributes; use the type as is, pydantic applies its defaults + # which do not accept new attributes; use the type as is, `validated_loads` passes the config + # to `TypeAdapter` directly in that case pass else: type_.__config__ = type_.__config__.copy() @@ -72,9 +73,13 @@ def type_with_config(type_: type[T], config: pydantic.ConfigDict) -> type[T]: 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 = type_with_config(expected_type, pydantic.ConfigDict(extra='allow')) + config = pydantic.ConfigDict(extra='allow') + expected_type = type_with_config(expected_type, config) + has_config = hasattr(expected_type, '__pydantic_config__') or hasattr( + expected_type, '__config__' + ) try: - ta = TypeAdapter(expected_type) + ta = TypeAdapter(expected_type, config=None if has_config else config) except TypeError: # TypeError: unsupported operand type(s) for |: 'type' and 'NoneType' # This is thrown on python<3.10 even with eval_type_backport diff --git a/changelog.d/+pydantic-generic-alias.fixed.md b/changelog.d/+pydantic-generic-alias.fixed.md index dbc47421f..7bfa3199d 100644 --- a/changelog.d/+pydantic-generic-alias.fixed.md +++ b/changelog.d/+pydantic-generic-alias.fixed.md @@ -1 +1 @@ -Fix `b2 bucket create --lifecycle-rule` (and other JSON options typed as a parametrized list) failing with `AttributeError: 'types.GenericAlias' object has no attribute '__pydantic_config__'` when the `full` extra (pydantic) is installed. +Fix `b2 bucket create` and `b2 bucket update` with the deprecated `--lifecycle-rules` option (and other JSON options typed as a parametrized list) failing with `AttributeError: 'types.GenericAlias' object has no attribute '__pydantic_config__'` when the `full` extra (pydantic) is installed. diff --git a/test/unit/_cli/test_obj_loads.py b/test/unit/_cli/test_obj_loads.py index cc1bd8399..f0502df6c 100644 --- a/test/unit/_cli/test_obj_loads.py +++ b/test/unit/_cli/test_obj_loads.py @@ -68,10 +68,11 @@ def test_validated_loads__typed_dict_types_validation(typed_dict_cls): validated_loads(input_, typed_dict_cls) +@pytest.mark.skipif(pydantic is None, reason='pydantic is not enabled') def test_validated_loads__parametrized_generic(typed_dict_cls): - # `list[...]` is a `types.GenericAlias`, which does not accept new attributes - input_ = '[{"a": 1, "b": "2"}]' - expected_val = [{'a': 1, 'b': '2'}] + # `list[...]` is a `types.GenericAlias`, which does not accept new attributes; extra keys must be kept + input_ = '[{"a": 1, "b": "2", "extra": null}]' + expected_val = [{'a': 1, 'b': '2', 'extra': None}] assert validated_loads(input_, list[typed_dict_cls]) == expected_val