Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions b2/_internal/_cli/obj_loads.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ 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, `validated_loads` passes the config
# to `TypeAdapter` directly in that case
pass
else:
type_.__config__ = type_.__config__.copy()
type_.__config__.update(config)
Expand All @@ -67,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
Expand Down
1 change: 1 addition & 0 deletions changelog.d/+pydantic-generic-alias.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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.
15 changes: 15 additions & 0 deletions test/unit/_cli/test_obj_loads.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,18 @@ 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)


@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; 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


@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])
Loading