From e95e7837a4837072000bc34e6c8c059c2ea76e1c Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Fri, 31 Dec 2021 14:44:09 +0000 Subject: [PATCH] apply `update_forward_refs` to `json_encoders` (#3595) * apply update_forward_refs to json_encoders, fix #3583 * linting * mypy * avoid use of ForwardRef with python3.6 * fix ForwardRef usage, take 2 * coverage --- changes/3583-samuelcolvin.md | 1 + pydantic/config.py | 3 +- pydantic/json.py | 5 +--- pydantic/main.py | 4 +-- pydantic/typing.py | 16 ++++++++++ tests/test_forward_ref.py | 57 ++++++++++++++++++++++++++++++++++++ 6 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 changes/3583-samuelcolvin.md diff --git a/changes/3583-samuelcolvin.md b/changes/3583-samuelcolvin.md new file mode 100644 index 00000000000..c8c3a627137 --- /dev/null +++ b/changes/3583-samuelcolvin.md @@ -0,0 +1 @@ +Apply `update_forward_refs` to `Config.json_encodes` prevent name clashes in types defined via strings. diff --git a/pydantic/config.py b/pydantic/config.py index 646e60ca8d5..b37cd98ff17 100644 --- a/pydantic/config.py +++ b/pydantic/config.py @@ -59,7 +59,8 @@ class BaseConfig: schema_extra: Union[Dict[str, Any], 'SchemaExtraCallable'] = {} json_loads: Callable[[str], Any] = json.loads json_dumps: Callable[..., str] = json.dumps - json_encoders: Dict[Type[Any], AnyCallable] = {} + # key type should include ForwardRef, but that breaks with python3.6 + json_encoders: Dict[Union[Type[Any], str], AnyCallable] = {} underscore_attrs_are_private: bool = False # whether inherited models as fields should be reconstructed as base model diff --git a/pydantic/json.py b/pydantic/json.py index d03f3b042a2..ce956fea263 100644 --- a/pydantic/json.py +++ b/pydantic/json.py @@ -103,10 +103,7 @@ def custom_pydantic_encoder(type_encoders: Dict[Any, Callable[[Type[Any]], Any]] try: encoder = type_encoders[base] except KeyError: - try: - encoder = type_encoders[base.__name__] - except KeyError: - continue + continue return encoder(obj) else: # We have exited the for loop without finding a suitable encoder diff --git a/pydantic/main.py b/pydantic/main.py index 8a37f9269f0..eea8abcbbe8 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -770,14 +770,14 @@ def __try_update_forward_refs__(cls) -> None: Same as update_forward_refs but will not raise exception when forward references are not defined. """ - update_model_forward_refs(cls, cls.__fields__.values(), {}, (NameError,)) + update_model_forward_refs(cls, cls.__fields__.values(), cls.__config__.json_encoders, {}, (NameError,)) @classmethod def update_forward_refs(cls, **localns: Any) -> None: """ Try to update ForwardRefs on fields based on this Model, globalns and localns. """ - update_model_forward_refs(cls, cls.__fields__.values(), localns) + update_model_forward_refs(cls, cls.__fields__.values(), cls.__config__.json_encoders, localns) def __iter__(self) -> 'TupleGenerator': """ diff --git a/pydantic/typing.py b/pydantic/typing.py index a98e120b470..730dc46442c 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -461,6 +461,7 @@ def update_field_forward_refs(field: 'ModelField', globalns: Any, localns: Any) def update_model_forward_refs( model: Type[Any], fields: Iterable['ModelField'], + json_encoders: Dict[Union[Type[Any], str], AnyCallable], localns: 'DictStrAny', exc_to_suppress: Tuple[Type[BaseException], ...] = (), ) -> None: @@ -480,6 +481,21 @@ def update_model_forward_refs( except exc_to_suppress: pass + for key in set(json_encoders.keys()): + if isinstance(key, str): + fr: ForwardRef = ForwardRef(key) + elif isinstance(key, ForwardRef): + fr = key + else: + continue + + try: + new_key = evaluate_forwardref(fr, globalns, localns or None) + except exc_to_suppress: # pragma: no cover + continue + + json_encoders[new_key] = json_encoders.pop(key) + def get_class(type_: Type[Any]) -> Union[None, bool, Type[Any]]: """ diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py index aac1dae10a4..df378810cd2 100644 --- a/tests/test_forward_ref.py +++ b/tests/test_forward_ref.py @@ -626,3 +626,60 @@ class Model(BaseModel): ) assert module.Model.__class_vars__ == {'a'} + + +@skip_pre_37 +def test_json_encoder_str(create_module): + module = create_module( + # language=Python + """ +from pydantic import BaseModel + + +class User(BaseModel): + x: str + + +FooUser = User + + +class User(BaseModel): + y: str + + +class Model(BaseModel): + foo_user: FooUser + user: User + + class Config: + json_encoders = { + 'User': lambda v: f'User({v.y})', + } +""" + ) + + m = module.Model(foo_user={'x': 'user1'}, user={'y': 'user2'}) + assert m.json(models_as_dict=False) == '{"foo_user": {"x": "user1"}, "user": "User(user2)"}' + + +@skip_pre_37 +def test_json_encoder_forward_ref(create_module): + module = create_module( + # language=Python + """ +from pydantic import BaseModel +from typing import ForwardRef, List, Optional + +class User(BaseModel): + name: str + friends: Optional[List['User']] = None + + class Config: + json_encoders = { + ForwardRef('User'): lambda v: f'User({v.name})', + } +""" + ) + + m = module.User(name='anne', friends=[{'name': 'ben'}, {'name': 'charlie'}]) + assert m.json(models_as_dict=False) == '{"name": "anne", "friends": ["User(ben)", "User(charlie)"]}'