8000 Add support for custom naming schemes for GenericModel subclasses (#860) · ag-python/pydantic@09baf53 · GitHub
[go: up one dir, main page]

Skip to content

Commit 09baf53

Browse files
dmontagusamuelcolvin
authored andcommitted
Add support for custom naming schemes for GenericModel subclasses (pydantic#860)
1 parent 425fac6 commit 09baf53

File tree

5 files changed

+52
-7
lines changed

5 files changed

+52
-7
lines changed

changes/859-dmontagu.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for custom naming schemes for `GenericModel` subclasses.

docs/examples/generics-naming.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import Generic, TypeVar, Type, Any, Tuple
2+
3+
from pydantic.generics import GenericModel
4+
5+
DataT = TypeVar('DataT')
6+
7+
class Response(GenericModel, Generic[DataT]):
8+
data: DataT
9+
10+
@classmethod
11+
def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str:
12+
return f'{params[0].__name__.title()}Response'
13+
14+
print(Response[int](data=1))
15+
# IntResponse data=1
16+
print(Response[str](data='a'))
17+
# StrResponse data='a'

docs/usage/models.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,13 @@ you would expect mypy to provide if you were to declare the type without using `
265265
Internally, pydantic uses `create_model` to generate a (cached) concrete `BaseModel` at runtime,
266266
so there is essentially zero overhead introduced by making use of `GenericModel`.
267267

268+
If the name of the concrete subclasses is important, you can also override the default behavior:
269+
270+
```py
271+
{!./examples/generics-naming.py!}
272+
```
273+
_(This script is complete, it should run "as is")_
274+
268275
## Dynamic model creation
269276

270277
There are some occasions where the shape of a model is not known until runtime, for this *pydantic* provides

pydantic/generics.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def __class_getitem__( # type: ignore
4040
k: resolve_type_hint(v, typevars_map) for k, v in instance_type_hints.items()
4141
}
4242

43-
model_name = concrete_name(cls, params)
43+
model_name = cls.__concrete_name__(params)
4444
validators = gather_all_validators(cls)
4545
fields: Dict[str, Tuple[Type[Any], Any]] = {
4646
k: (v, cls.__fields__[k].field_info) for k, v in concrete_type_hints.items() if k in cls.__fields__
@@ -60,11 +60,14 @@ def __class_getitem__( # type: ignore
6060
_generic_types_cache[(cls, params[0])] = created_model
6161
return created_model
6262

63-
64-
def concrete_name(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str:
65-
param_names = [param.__name__ if hasattr(param, '__name__') else str(param) for param in params]
66-
params_component = ', '.join(param_names)
67-
return f'{cls.__name__}[{params_component}]'
63+
@classmethod
64+
def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str:
65+
"""
66+
This method can be overridden to achieve a custom naming scheme for GenericModels
67+
"""
68+
param_names = [param.__name__ if hasattr(param, '__name__') else str(param) for param in params]
69+
params_component = ', '.join(param_names)
70+
return f'{cls.__name__}[{params_component}]'
6871

6972

7073
def resolve_type_hint(type_: Any, typevars_map: Dict[Any, Any]) -> Type[Any]:

tests/test_generics.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import sys
22
from enum import Enum
3-
from typing import Any, ClassVar, Dict, Generic, List, Optional, TypeVar, Union
3+
from typing import Any, ClassVar, Dict, Generic, List, Optional, Tuple, Type, TypeVar, Union
44

55
import pytest
66

@@ -421,3 +421,20 @@ class MyModel(GenericModel, Generic[T]):
421421

422422
schema = MyModel[int].schema()
423423
assert schema['properties']['a'].get('description') == 'Custom'
424+
425+
426+
@skip_36
427+
def test_custom_generic_naming():
428+
T = TypeVar('T')
429+
430+
class MyModel(GenericModel, Generic[T]):
431+
value: Optional[T]
432+
433+
@classmethod
434+
def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str:
435+
param_names = [param.__name__ if hasattr(param, '__name__') else str(param) for param in params]
436+
title = param_names[0].title()
437+
return f'Optional{title}Wrapper'
438+
439+
assert str(MyModel[int](value=1)) == 'OptionalIntWrapper value=1'
440+
assert str(MyModel[str](value=None)) == 'OptionalStrWrapper value=None'

0 commit comments

Comments
 (0)
0