Skip to content

Commit 87da9ac

Browse files
authored
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
1 parent 6f26a1c commit 87da9ac

File tree

6 files changed

+79
-7
lines changed

6 files changed

+79
-7
lines changed

changes/3583-samuelcolvin.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Apply `update_forward_refs` to `Config.json_encodes` prevent name clashes in types defined via strings.

pydantic/config.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ class BaseConfig:
5959
schema_extra: Union[Dict[str, Any], 'SchemaExtraCallable'] = {}
6060
json_loads: Callable[[str], Any] = json.loads
6161
json_dumps: Callable[..., str] = json.dumps
62-
json_encoders: Dict[Type[Any], AnyCallable] = {}
62+
# key type should include ForwardRef, but that breaks with python3.6
63+
json_encoders: Dict[Union[Type[Any], str], AnyCallable] = {}
6364
underscore_attrs_are_private: bool = False
6465

6566
# whether inherited models as fields should be reconstructed as base model

pydantic/json.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,7 @@ def custom_pydantic_encoder(type_encoders: Dict[Any, Callable[[Type[Any]], Any]]
103103
try:
104104
encoder = type_encoders[base]
105105
except KeyError:
106-
try:
107-
encoder = type_encoders[base.__name__]
108-
except KeyError:
109-
continue
106+
continue
110107

111108
return encoder(obj)
112109
else: # We have exited the for loop without finding a suitable encoder

pydantic/main.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -770,14 +770,14 @@ def __try_update_forward_refs__(cls) -> None:
770770
Same as update_forward_refs but will not raise exception
771771
when forward references are not defined.
772772
"""
773-
update_model_forward_refs(cls, cls.__fields__.values(), {}, (NameError,))
773+
update_model_forward_refs(cls, cls.__fields__.values(), cls.__config__.json_encoders, {}, (NameError,))
774774

775775
@classmethod
776776
def update_forward_refs(cls, **localns: Any) -> None:
777777
"""
778778
Try to update ForwardRefs on fields based on this Model, globalns and localns.
779779
"""
780-
update_model_forward_refs(cls, cls.__fields__.values(), localns)
780+
update_model_forward_refs(cls, cls.__fields__.values(), cls.__config__.json_encoders, localns)
781781

782782
def __iter__(self) -> 'TupleGenerator':
783783
"""

pydantic/typing.py

+16
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ def update_field_forward_refs(field: 'ModelField', globalns: Any, localns: Any)
461461
def update_model_forward_refs(
462462
model: Type[Any],
463463
fields: Iterable['ModelField'],
464+
json_encoders: Dict[Union[Type[Any], str], AnyCallable],
464465
localns: 'DictStrAny',
465466
exc_to_suppress: Tuple[Type[BaseException], ...] = (),
466467
) -> None:
@@ -480,6 +481,21 @@ def update_model_forward_refs(
480481
except exc_to_suppress:
481482
pass
482483

484+
for key in set(json_encoders.keys()):
485+
if isinstance(key, str):
486+
fr: ForwardRef = ForwardRef(key)
487+
elif isinstance(key, ForwardRef):
488+
fr = key
489+
else:
490+
continue
491+
492+
try:
493+
new_key = evaluate_forwardref(fr, globalns, localns or None)
494+
except exc_to_suppress: # pragma: no cover
495+
continue
496+
497+
json_encoders[new_key] = json_encoders.pop(key)
498+
483499

484500
def get_class(type_: Type[Any]) -> Union[None, bool, Type[Any]]:
485501
"""

tests/test_forward_ref.py

+57
Original file line numberDiff line numberDiff line change
@@ -626,3 +626,60 @@ class Model(BaseModel):
626626
)
627627

628628
assert module.Model.__class_vars__ == {'a'}
629+
630+
631+
@skip_pre_37
632+
def test_json_encoder_str(create_module):
633+
module = create_module(
634+
# language=Python
635+
"""
636+
from pydantic import BaseModel
637+
638+
639+
class User(BaseModel):
640+
x: str
641+
642+
643+
FooUser = User
644+
645+
646+
class User(BaseModel):
647+
y: str
648+
649+
650+
class Model(BaseModel):
651+
foo_user: FooUser
652+
user: User
653+
654+
class Config:
655+
json_encoders = {
656+
'User': lambda v: f'User({v.y})',
657+
}
658+
"""
659+
)
660+
661+
m = module.Model(foo_user={'x': 'user1'}, user={'y': 'user2'})
662+
assert m.json(models_as_dict=False) == '{"foo_user": {"x": "user1"}, "user": "User(user2)"}'
663+
664+
665+
@skip_pre_37
666+
def test_json_encoder_forward_ref(create_module):
667+
module = create_module(
668+
# language=Python
669+
"""
670+
from pydantic import BaseModel
671+
from typing import ForwardRef, List, Optional
672+
673+
class User(BaseModel):
674+
name: str
675+
friends: Optional[List['User']] = None
676+
677+
class Config:
678+
json_encoders = {
679+
ForwardRef('User'): lambda v: f'User({v.name})',
680+
}
681+
"""
682+
)
683+
684+
m = module.User(name='anne', friends=[{'name': 'ben'}, {'name': 'charlie'}])
685+
assert m.json(models_as_dict=False) == '{"name": "anne", "friends": ["User(ben)", "User(charlie)"]}'

0 commit comments

Comments
 (0)