Skip to content

Commit ca73a5b

Browse files
committed
perf: cache model fields
1 parent 76e2c5c commit ca73a5b

File tree

8 files changed

+59
-33
lines changed

8 files changed

+59
-33
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
@@ -411,35 +411,33 @@ def is_supported_type(cls, value: Any) -> TypeGuard[type[T]]:
411411
return _is_pydantic_v1_model(value) or _is_pydantic_v2_model(value)
412412

413413
@classmethod
414+
@cache_model_fields
414415
def get_model_fields(cls) -> list["FieldMeta"]:
415416
"""Retrieve a list of fields from the factory's model.
416417
417418
418419
:returns: A list of field MetaData instances.
419420
420421
"""
421-
if "_fields_metadata" not in cls.__dict__:
422-
if _is_pydantic_v1_model(cls.__model__):
423-
cls._fields_metadata = [
424-
PydanticFieldMeta.from_model_field(
425-
field,
426-
use_alias=not cls.__model__.__config__.allow_population_by_field_name, # type: ignore[attr-defined]
427-
)
428-
for field in cls.__model__.__fields__.values()
429-
]
430-
else:
431-
use_alias = cls.__model__.model_config.get("validate_by_name", False) or cls.__model__.model_config.get(
432-
"populate_by_name", False
422+
if _is_pydantic_v1_model(cls.__model__):
423+
return [
424+
PydanticFieldMeta.from_model_field(
425+
field,
426+
use_alias=not cls.__model__.__config__.allow_population_by_field_name, # type: ignore[attr-defined]
433427
)
434-
cls._fields_metadata = [
435-
PydanticFieldMeta.from_field_info(
436-
field_info=field_info,
437-
field_name=field_name,
438-
use_alias=not use_alias,
439-
)
440-
for field_name, field_info in cls.__model__.model_fields.items() # pyright: ignore[reportGeneralTypeIssues]
441-
]
442-
return cls._fields_metadata
428+
for field in cls.__model__.__fields__.values()
429+
]
430+
use_alias = cls.__model__.model_config.get("validate_by_name", False) or cls.__model__.model_config.get(
431+
"populate_by_name", False
432+
)
433+
return [
434+
PydanticFieldMeta.from_field_info(
435+
field_info=field_info,
436+
field_name=field_name,
437+
use_alias=not use_alias,
438+
)
439+
for field_name, field_info in cls.__model__.model_fields.items() # pyright: ignore[reportGeneralTypeIssues]
440+
]
443441

444442
@classmethod
445443
def get_constrained_field_value(

polyfactory/factories/sqlalchemy_factory.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
from polyfactory.exceptions import ConfigurationException, MissingDependencyException, ParameterException
2020
from polyfactory.factories.base import BaseFactory
21+
from polyfactory.exceptions import MissingDependencyException, ParameterException
22+
from polyfactory.factories.base import BaseFactory, cache_model_fields
2123
from polyfactory.field_meta import Constraints, FieldMeta
2224
from polyfactory.persistence import AsyncPersistenceProtocol, SyncPersistenceProtocol
2325
from polyfactory.utils.types import Frozendict
@@ -241,6 +243,7 @@ def get_type_from_collection_class(
241243
return annotation
242244

243245
@classmethod
246+
@cache_model_fields
244247
def get_model_fields(cls) -> list[FieldMeta]:
245248
fields_meta: list[FieldMeta] = []
246249

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)