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

adds model_name option in ConfigDict #6814

Closed
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions pydantic/_internal/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class ConfigWrapper:
# all annotations are copied directly from ConfigDict, and should be kept up to date, a test will fail if they
# stop matching
title: str | None
model_name: str | None
str_to_lower: bool
str_to_upper: bool
str_strip_whitespace: bool
Expand Down Expand Up @@ -217,6 +218,7 @@ def push(self, config_wrapper: ConfigWrapper | ConfigDict | None):

config_defaults = ConfigDict(
title=None,
model_name=None,
str_to_lower=False,
str_to_upper=False,
str_strip_whitespace=False,
Expand Down
1 change: 1 addition & 0 deletions pydantic/_internal/_model_construction.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def __new__(
base_field_names, class_vars, base_private_attributes = mcs._collect_bases_data(bases)

config_wrapper = ConfigWrapper.for_model(bases, namespace, kwargs)
cls_name = config_wrapper.config_dict.get('model_name') or cls_name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to have some kind of "backup" of the original class name if overridden by config.model_name? I'm making use of cls.__name__ here, and comparing it to AST-parsed file.

In fact it would be great to have this PR merged before #6563, I will then be able to rebase on top of it and add this "backup" field

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that if cls.__name__ is used somewhere else, maybe it would be better to keep its original behaviour and create an extra attribute for the model_name that will explicitly interact with the v1.schema functions. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this seems better: __name__ is a Python implementation detail and people should expect it to be the same as defined in the source code (it might also be used by documentation tools). Perhaps you can define a specific attribute that will be used for schema generation

namespace['model_config'] = config_wrapper.config_dict
private_attributes = inspect_namespace(
namespace, config_wrapper.ignored_types, class_vars, base_field_names
Expand Down
3 changes: 3 additions & 0 deletions pydantic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ class ConfigDict(TypedDict, total=False):
title: str | None
"""The title for the generated JSON schema, defaults to the model's name"""

model_name: str | None
"""If not `None` overrides the model names generated by `pydantic.v1.schema.get_model_name_map()`."""
romulocollopy marked this conversation as resolved.
Show resolved Hide resolved

str_to_lower: bool
"""Whether to convert all characters to lowercase for str types. Defaults to `False`."""

Expand Down
3 changes: 2 additions & 1 deletion pydantic/v1/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@
default_prefix = '#/definitions/'
default_ref_template = '#/definitions/{model}'

TypeModelOrEnum = Union[Type['BaseModel'], Type[Enum]]
T = TypeVar('T', bound='BaseModel')
TypeModelOrEnum = Union[Type[T], Type[Enum]]
sydney-runkle marked this conversation as resolved.
Show resolved Hide resolved
TypeModelSet = Set[TypeModelOrEnum]


Expand Down
61 changes: 61 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from pydantic.errors import PydanticUserError
from pydantic.fields import FieldInfo
from pydantic.type_adapter import TypeAdapter
from pydantic.v1.schema import get_model_name_map
from pydantic.warnings import PydanticDeprecationWarning

if sys.version_info < (3, 9):
Expand Down Expand Up @@ -492,6 +493,66 @@ def my_function():
pass


def test_config_model_name() -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps I'm missing something here, but isn't the only thing that we really need to focus on testing just that the name of the model when assigned via model_name is correct? Why even bring in get_model_name_map?

You'll definitely want to run a test or two with model names not equivalent to what they would by default be.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of this PR was to address #6304, but considering this comment, maybe it is better to not touch the cls.__name__ and change the behaviour of get_model_name_map by making it check a new attribute with the model_name.

I would rework a bit the implementation in this direction and keep this test. What do you think?

CLIENT_USER_MODEL_NAME = 'ClientUser'
BUSINESS_USER_MODEL_NAME = 'BusinessUser'

def _get_business_user_class():
class User(BaseModel):
model_config = ConfigDict(model_name=BUSINESS_USER_MODEL_NAME)

return User

def _get_client_user_class():
class User(BaseModel):
model_config = ConfigDict(model_name=CLIENT_USER_MODEL_NAME)

return User

BusinessUser = _get_business_user_class()
ClientUser = _get_client_user_class()

name_map = get_model_name_map({BusinessUser, ClientUser})
assert name_map[BusinessUser] == BUSINESS_USER_MODEL_NAME
assert name_map[ClientUser] == CLIENT_USER_MODEL_NAME

assert BusinessUser().model_json_schema()['title'] == BUSINESS_USER_MODEL_NAME
assert ClientUser().model_json_schema()['title'] == CLIENT_USER_MODEL_NAME


def test_config_model_name__long_model_name_on_conflict() -> None:
CLIENT_USER_MODEL_NAME = 'ClientUser'

def _get_client_user_class():
class User(BaseModel):
model_config = ConfigDict(model_name=CLIENT_USER_MODEL_NAME)

return User

def _get_client_2_user_class():
class User(BaseModel):
model_config = ConfigDict(model_name=CLIENT_USER_MODEL_NAME)

return User

ClientUser = _get_client_user_class()
ClientUserV2 = _get_client_2_user_class()

name_map = get_model_name_map({ClientUser, ClientUserV2})
assert (
name_map[ClientUser]
== 'tests__test_config__test_config_model_name__long_model_name_on_conflict__<locals>___get_client_user_class__<locals>__User'
)
assert (
name_map[ClientUserV2]
== 'tests__test_config__test_config_model_name__long_model_name_on_conflict__<locals>___get_client_2_user_class__<locals>__User'
)

# FIXME: Maye the model_json_schema should have a long name on conflict?
assert ClientUser().model_json_schema()['title'] == CLIENT_USER_MODEL_NAME
assert ClientUserV2().model_json_schema()['title'] == CLIENT_USER_MODEL_NAME


def test_multiple_inheritance_config():
class Parent(BaseModel):
model_config = ConfigDict(frozen=True, extra='forbid')
Expand Down