Skip to content

Commit cec1eca

Browse files
committed
perf: cache model fields
1 parent 6ab574a commit cec1eca

File tree

8 files changed

+58
-34
lines changed

8 files changed

+58
-34
lines changed

polyfactory/factories/attrs_factory.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import TYPE_CHECKING, Generic, TypeVar
55

66
from polyfactory.exceptions import MissingDependencyException
7-
from polyfactory.factories.base import BaseFactory
7+
from polyfactory.factories.base import BaseFactory, cache_model_fields
88
from polyfactory.field_meta import FieldMeta, Null
99

1010
if TYPE_CHECKING:
@@ -35,13 +35,19 @@ def is_supported_type(cls, value: Any) -> TypeGuard[type[T]]:
3535
return isclass(value) and hasattr(value, "__attrs_attrs__")
3636

3737
@classmethod
38+
def _init_model(cls) -> None:
39+
"""Initialize the model and resolve type annotations."""
40+
super()._init_model()
41+
if hasattr(cls, "__model__"):
42+
cls.resolve_types(cls.__model__)
43+
44+
@classmethod
45+
@cache_model_fields
3846
def get_model_fields(cls) -> list[FieldMeta]:
3947
field_metas: list[FieldMeta] = []
4048
none_type = type(None)
4149

42-
cls.resolve_types(cls.__model__)
4350
fields = attrs.fields(cls.__model__)
44-
4551
for field in fields:
4652
if not field.init:
4753
continue

polyfactory/factories/base.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import copy
4+
import functools
45
import inspect
56
from abc import ABC, abstractmethod
67
from collections import Counter, abc, deque
@@ -97,6 +98,22 @@
9798
F = TypeVar("F", bound="BaseFactory[Any]")
9899

99100

101+
def cache_model_fields(func: Callable[[type[F]], list["FieldMeta"]]) -> Callable[[type[F]], list["FieldMeta"]]:
102+
"""Decorator to cache the results of get_model_fields() to avoid repeated introspection.
103+
104+
:param func: The get_model_fields classmethod to wrap
105+
:returns: Wrapped function with caching
106+
"""
107+
108+
@functools.wraps(func)
109+
def wrapper(cls: type[F]) -> list["FieldMeta"]:
110+
if "_fields_metadata" not in cls.__dict__:
111+
cls._fields_metadata = func(cls)
112+
return cls._fields_metadata
113+
114+
return wrapper
115+
116+
100117
class BuildContext(TypedDict):
101118
seen_models: set[type]
102119

@@ -124,12 +141,13 @@ class BaseFactory(ABC, Generic[T]):
124141
"""A sync persistence handler. Can be a class or a class instance."""
125142
__async_persistence__: type[AsyncPersistenceProtocol[T]] | AsyncPersistenceProtocol[T] | None = None
126143
"""An async persistence handler. Can be a class or a class instance."""
127-
__set_as_default_factory_for_type__ = False
144+
145+
__set_as_default_factory_for_type__: ClassVar[bool] = False
128146
"""
129147
Flag dictating whether to set as the default factory for the given type.
130148
If 'True' the factory will be used instead of dynamically generating a factory for the type.
131149
"""
132-
__is_base_factory__: bool = False
150+
__is_base_factory__: ClassVar[bool] = False
133151
"""
134152
Flag dictating whether the factory is a 'base' factory. Base factories are registered globally as handlers for types.
135153
For example, the 'DataclassFactory', 'TypedDictFactory' and 'ModelFactory' are all base factories.

polyfactory/factories/dataclass_factory.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from typing_extensions import TypeGuard
77

8-
from polyfactory.factories.base import BaseFactory, T
8+
from polyfactory.factories.base import BaseFactory, T, cache_model_fields
99
from polyfactory.field_meta import FieldMeta, Null
1010

1111

@@ -24,6 +24,7 @@ def is_supported_type(cls, value: Any) -> TypeGuard[type[T]]:
2424
return bool(is_dataclass(value))
2525

2626
@classmethod
27+
@cache_model_fields
2728
def get_model_fields(cls) -> list["FieldMeta"]:
2829
"""Retrieve a list of fields from the factory's model.
2930

polyfactory/factories/msgspec_factory.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, get_type_hints
55

66
from polyfactory.exceptions import MissingDependencyException
7-
from polyfactory.factories.base import BaseFactory
7+
from polyfactory.factories.base import BaseFactory, cache_model_fields
88
from polyfactory.field_meta import FieldMeta, Null
99
from polyfactory.value_generators.constrained_numbers import handle_constrained_int
1010
from polyfactory.value_generators.primitives import create_random_bytes
@@ -46,6 +46,7 @@ def is_supported_type(cls, value: Any) -> TypeGuard[type[T]]:
4646
return isclass(value) and hasattr(value, "__struct_fields__")
4747

4848
@classmethod
49+
@cache_model_fields
4950
def get_model_fields(cls) -> list[FieldMeta]:
5051
fields_meta: list[FieldMeta] = []
5152

polyfactory/factories/pydantic_factory.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from typing_extensions import Literal, get_args
1313

1414
from polyfactory.exceptions import MissingDependencyException
15-
from polyfactory.factories.base import BaseFactory, BuildContext
15+
from polyfactory.factories.base import BaseFactory, BuildContext, cache_model_fields
1616
from polyfactory.factories.base import BuildContext as BaseBuildContext
1717
from polyfactory.field_meta import Constraints, FieldMeta, Null
1818
from polyfactory.utils.helpers import unwrap_new_type, unwrap_optional
@@ -407,35 +407,33 @@ def is_supported_type(cls, value: Any) -> TypeGuard[type[T]]:
407407
return _is_pydantic_v1_model(value) or _is_pydantic_v2_model(value)
408408

409409
@classmethod
410+
@cache_model_fields
410411
def get_model_fields(cls) -> list["FieldMeta"]:
411412
"""Retrieve a list of fields from the factory's model.
412413
413414
414415
:returns: A list of field MetaData instances.
415416
416417
"""
417-
if "_fields_metadata" not in cls.__dict__:
418-
if _is_pydantic_v1_model(cls.__model__):
419-
cls._fields_metadata = [
420-
PydanticFieldMeta.from_model_field(
421-
field,
422-
use_alias=not cls.__model__.__config__.allow_population_by_field_name, # type: ignore[attr-defined]
423-
)
424-
for field in cls.__model__.__fields__.values()
425-
]
426-
else:
427-
use_alias = cls.__model__.model_config.get("validate_by_name", False) or cls.__model__.model_config.get(
428-
"populate_by_name", False
418+
if _is_pydantic_v1_model(cls.__model__):
419+
return [
420+
PydanticFieldMeta.from_model_field(
421+
field,
422+
use_alias=not cls.__model__.__config__.allow_population_by_field_name, # type: ignore[attr-defined]
429423
)
430-
cls._fields_metadata = [
431-
PydanticFieldMeta.from_field_info(
432-
field_info=field_info,
433-
field_name=field_name,
434-
use_alias=not use_alias,
435-
)
436-
for field_name, field_info in cls.__model__.model_fields.items() # pyright: ignore[reportGeneralTypeIssues]
437-
]
438-
return cls._fields_metadata
424+
for field in cls.__model__.__fields__.values()
425+
]
426+
use_alias = cls.__model__.model_config.get("validate_by_name", False) or cls.__model__.model_config.get(
427+
"populate_by_name", False
428+
)
429+
return [
430+
PydanticFieldMeta.from_field_info(
431+
field_info=field_info,
432+
field_name=field_name,
433+
use_alias=not use_alias,
434+
)
435+
for field_name, field_info in cls.__model__.model_fields.items() # pyright: ignore[reportGeneralTypeIssues]
436+
]
439437

440438
@classmethod
441439
def get_constrained_field_value(

polyfactory/factories/sqlalchemy_factory.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import TYPE_CHECKING, Annotated, Any, Callable, ClassVar, Generic, Protocol, TypeVar, Union
55

66
from polyfactory.exceptions import MissingDependencyException, ParameterException
7-
from polyfactory.factories.base import BaseFactory
7+
from polyfactory.factories.base import BaseFactory, cache_model_fields
88
from polyfactory.field_meta import Constraints, FieldMeta
99
from polyfactory.persistence import AsyncPersistenceProtocol, SyncPersistenceProtocol
1010
from polyfactory.utils.types import Frozendict
@@ -204,6 +204,7 @@ def get_type_from_column(cls, column: Column) -> type:
204204
return annotation
205205

206206
@classmethod
207+
@cache_model_fields
207208
def get_model_fields(cls) -> list[FieldMeta]:
208209
fields_meta: list[FieldMeta] = []
209210

polyfactory/factories/typed_dict_factory.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
is_typeddict,
1212
)
1313

14-
from polyfactory.factories.base import BaseFactory
14+
from polyfactory.factories.base import BaseFactory, cache_model_fields
1515
from polyfactory.field_meta import FieldMeta, Null
1616

1717
TypedDictT = TypeVar("TypedDictT", bound=_TypedDictMeta)
@@ -32,6 +32,7 @@ def is_supported_type(cls, value: Any) -> TypeGuard[type[TypedDictT]]:
3232
return is_typeddict(value)
3333

3434
@classmethod
35+
@cache_model_fields
3536
def get_model_fields(cls) -> list["FieldMeta"]:
3637
"""Retrieve a list of fields from the factory's model.
3738

polyfactory/field_meta.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import datetime
2323
from collections.abc import Sequence
2424
from decimal import Decimal
25-
from random import Random
2625
from re import Pattern
2726

2827
from typing_extensions import NotRequired, Self
@@ -68,10 +67,9 @@ class Constraints(TypedDict):
6867
class FieldMeta:
6968
"""Factory field metadata container. This class is used to store the data about a field of a factory's model."""
7069

71-
__slots__ = ("__dict__", "annotation", "children", "constraints", "default", "name", "random")
70+
__slots__ = ("__dict__", "annotation", "children", "constraints", "default", "name")
7271

7372
annotation: Any
74-
random: Random
7573
children: list[FieldMeta] | None
7674
default: Any
7775
name: str

0 commit comments

Comments
 (0)