From 7c8bf4d688788179b07442e8dd1f1a7c173596d6 Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 11:13:10 +0100 Subject: [PATCH] Refactor `Dependant.is_gen_callable` property for improved memory usage ``` Samples: 200000 - current=318.4 MB peak=318.4 MB Samples: 100000 - current=159.2 MB peak=159.2 MB Samples: 50000 - current=79.6 MB peak=79.6 MB Samples: 1000 - current=1.6 MB peak=1.6 MB Samples: 500 - current=0.8 MB peak=0.8 MB ``` --- fastapi/dependencies/models.py | 63 +++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index aeda28ac15..c2737df23a 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -56,6 +56,7 @@ class Dependant: _is_security_scheme_cache: bool = field(default=None, init=False, repr=False) _security_scheme_cache: SecurityBase = field(default=None, init=False, repr=False) _security_dependencies_cache: list["Dependant"] = field(default=None, init=False, repr=False) + _is_gen_callable_cache: bool = field(default=None, init=False, repr=False) @property def oauth_scopes(self) -> list[str]: @@ -132,31 +133,45 @@ class Dependant: return self._security_dependencies_cache - @cached_property + @property def is_gen_callable(self) -> bool: - if self.call is None: - return False # pragma: no cover - if inspect.isgeneratorfunction( - _impartial(self.call) - ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)): - return True - if inspect.isclass(_unwrapped_call(self.call)): - return False - dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 - if dunder_call is None: - return False # pragma: no cover - if inspect.isgeneratorfunction( - _impartial(dunder_call) - ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)): - return True - dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 - if dunder_unwrapped_call is None: - return False # pragma: no cover - if inspect.isgeneratorfunction( - _impartial(dunder_unwrapped_call) - ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call)): - return True - return False + if self._is_gen_callable_cache is None: + if self.call is None: + self._is_gen_callable_cache = False # pragma: no cover + elif inspect.isgeneratorfunction( + _impartial(self.call) + ) or inspect.isgeneratorfunction(_unwrapped_call(self.call)): + self._is_gen_callable_cache = True + elif inspect.isclass(_unwrapped_call(self.call)): + self._is_gen_callable_cache = False + + # optimization to exit early + if self._is_gen_callable_cache is not None: + return self._is_gen_callable_cache + + dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 + if dunder_call is None: + self._is_gen_callable_cache = False # pragma: no cover + elif inspect.isgeneratorfunction( + _impartial(dunder_call) + ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_call)): + self._is_gen_callable_cache = True + + # optimization to exit early + if self._is_gen_callable_cache is not None: + return self._is_gen_callable_cache + + dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + if dunder_unwrapped_call is None: + self._is_gen_callable_cache = False # pragma: no cover + if inspect.isgeneratorfunction( + _impartial(dunder_unwrapped_call) + ) or inspect.isgeneratorfunction(_unwrapped_call(dunder_unwrapped_call)): + self._is_gen_callable_cache = True + else: + self._is_gen_callable_cache = False + + return self._is_gen_callable_cache @cached_property def is_async_gen_callable(self) -> bool: