Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

apply update_forward_refs to json_encoders #3595

Merged
merged 6 commits into from Dec 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/3583-samuelcolvin.md
@@ -0,0 +1 @@
Apply `update_forward_refs` to `Config.json_encodes` prevent name clashes in types defined via strings.
3 changes: 2 additions & 1 deletion pydantic/config.py
Expand Up @@ -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
Expand Down
5 changes: 1 addition & 4 deletions pydantic/json.py
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pydantic/main.py
Expand Up @@ -769,14 +769,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':
"""
Expand Down
16 changes: 16 additions & 0 deletions pydantic/typing.py
Expand Up @@ -459,6 +459,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:
Expand All @@ -478,6 +479,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]]:
"""
Expand Down
57 changes: 57 additions & 0 deletions tests/test_forward_ref.py
Expand Up @@ -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)"]}'