From dd109d369587eb91f1a2788fe6d430ccbc82e321 Mon Sep 17 00:00:00 2001 From: ipeluffo Date: Tue, 14 Apr 2026 11:15:40 +0100 Subject: [PATCH] Refactor `Dependant.is_async_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 | 65 ++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index c2737df23a..a19afd2fa8 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -57,6 +57,7 @@ class Dependant: _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) + _is_async_gen_callable_cache: bool = field(default=None, init=False, repr=False) @property def oauth_scopes(self) -> list[str]: @@ -145,7 +146,6 @@ class Dependant: 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 @@ -157,7 +157,6 @@ class Dependant: ) 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 @@ -170,34 +169,46 @@ class Dependant: self._is_gen_callable_cache = True else: self._is_gen_callable_cache = False - + return self._is_gen_callable_cache - @cached_property + @property def is_async_gen_callable(self) -> bool: - if self.call is None: - return False # pragma: no cover - if inspect.isasyncgenfunction( - _impartial(self.call) - ) or inspect.isasyncgenfunction(_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.isasyncgenfunction( - _impartial(dunder_call) - ) or inspect.isasyncgenfunction(_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.isasyncgenfunction( - _impartial(dunder_unwrapped_call) - ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_unwrapped_call)): - return True - return False + if self._is_async_gen_callable_cache is None: + if self.call is None: + self._is_async_gen_callable_cache = False # pragma: no cover + elif inspect.isasyncgenfunction( + _impartial(self.call) + ) or inspect.isasyncgenfunction(_unwrapped_call(self.call)): + self._is_async_gen_callable_cache = True + elif inspect.isclass(_unwrapped_call(self.call)): + self._is_async_gen_callable_cache = False + + if self._is_async_gen_callable_cache is not None: + return self._is_async_gen_callable_cache + + dunder_call = getattr(_impartial(self.call), "__call__", None) # noqa: B004 + if dunder_call is None: + self._is_async_gen_callable_cache = False # pragma: no cover + elif inspect.isasyncgenfunction( + _impartial(dunder_call) + ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_call)): + self._is_async_gen_callable_cache = True + + if self._is_async_gen_callable_cache is not None: + return self._is_async_gen_callable_cache + + dunder_unwrapped_call = getattr(_unwrapped_call(self.call), "__call__", None) # noqa: B004 + if dunder_unwrapped_call is None: + self._is_async_gen_callable_cache = False # pragma: no cover + elif inspect.isasyncgenfunction( + _impartial(dunder_unwrapped_call) + ) or inspect.isasyncgenfunction(_unwrapped_call(dunder_unwrapped_call)): + self._is_async_gen_callable_cache = True + else: + self._is_async_gen_callable_cache = False + + return self._is_async_gen_callable_cache @cached_property def is_coroutine_callable(self) -> bool: