diff --git a/setup.cfg b/setup.cfg index 93228729..0eaf6935 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,7 +40,6 @@ install_requires = requests jinja2 mappyfile - methodtools jsonpath-rw orjson more-ds diff --git a/src/schematools/_utils.py b/src/schematools/_utils.py new file mode 100644 index 00000000..11b1aabd --- /dev/null +++ b/src/schematools/_utils.py @@ -0,0 +1,31 @@ +"""Internal utils, not meant to be used outside schematools.""" + +import functools +import weakref + + +def cached_method(*lru_args, **lru_kwargs): + """A simple lru-cache per object. + This removed the need for methodtools.lru_cache(), which uses wirerope for purity. + The usage of wirerope started showing up as 5% of the request time, hence it's significant to remove. + """ + + def decorator(func): + @functools.wraps(func) + def initial_wrapped_func(self, *args, **kwargs): + # Not storing the wrapped method inside the instance. If we had + # a strong reference to self the instance would never die. + self_weak = weakref.ref(self) + + @functools.wraps(func) + @functools.lru_cache(*lru_args, **lru_kwargs) + def cached_method(*args, **kwargs): + return func(self_weak(), *args, **kwargs) + + # Assigns to the self reference (preserving the cache), and optimizes the next access. + setattr(self, func.__name__, cached_method) + return cached_method(*args, **kwargs) + + return initial_wrapped_func + + return decorator diff --git a/src/schematools/permissions/auth.py b/src/schematools/permissions/auth.py index fb3a34b1..ca8d52f9 100644 --- a/src/schematools/permissions/auth.py +++ b/src/schematools/permissions/auth.py @@ -3,12 +3,12 @@ The :class:`UserScopes` class handles whether a dataset, table or field can be accessed. The other classes in this module ease to retrieval of permission objects. """ + from __future__ import annotations from typing import Iterable, Iterator -import methodtools - +from schematools._utils import cached_method from schematools.types import ( DatasetFieldSchema, DatasetSchema, @@ -74,7 +74,7 @@ def add_query_params(self, params: list[str]): """ self._query_param_names.extend(params) - @methodtools.lru_cache() # type: ignore[misc] + @cached_method() # type: ignore[misc] def has_all_scopes(self, needed_scopes: frozenset[str]) -> bool: """Check whether the request has all scopes. @@ -82,7 +82,7 @@ def has_all_scopes(self, needed_scopes: frozenset[str]) -> bool: """ return self._scopes.issuperset(needed_scopes) - @methodtools.lru_cache() # type: ignore[misc] + @cached_method() # type: ignore[misc] def has_any_scope(self, needed_scopes: frozenset[str]) -> bool: """Check whether the request grants one of the given scopes. @@ -151,7 +151,7 @@ def _has_field_auth_access(self, field: DatasetFieldSchema) -> Permission: else: return Permission.none - @methodtools.lru_cache() + @cached_method() def _has_dataset_profile_access(self, dataset_id: str) -> Permission: """Give the permission access level for a dataset, as defined by the profile.""" return max( @@ -162,7 +162,7 @@ def _has_dataset_profile_access(self, dataset_id: str) -> Permission: default=Permission.none, ) - @methodtools.lru_cache() + @cached_method() def _has_table_profile_access(self, table: DatasetTableSchema) -> Permission: """Give the permission level for a table. @@ -239,7 +239,7 @@ def _has_field_profile_access(self, field: DatasetFieldSchema) -> Permission: return max_permission - @methodtools.lru_cache() + @cached_method() def get_active_profile_datasets(self, dataset_id: str) -> list[ProfileDatasetSchema]: """Find all profiles that mention a dataset and match the scopes. @@ -260,7 +260,7 @@ def get_active_profile_datasets(self, dataset_id: str) -> list[ProfileDatasetSch and (profile_dataset := profile.datasets.get(dataset_id)) is not None ] - @methodtools.lru_cache() + @cached_method() def get_active_profile_tables( self, dataset_id: str, table_id: str ) -> list[ProfileTableSchema]: diff --git a/src/schematools/types.py b/src/schematools/types.py index 41faf503..82a1681a 100644 --- a/src/schematools/types.py +++ b/src/schematools/types.py @@ -29,9 +29,8 @@ cast, ) -from methodtools import lru_cache - from schematools import MAX_TABLE_NAME_LENGTH +from schematools._utils import cached_method from schematools.exceptions import ( DatasetFieldNotFound, DatasetTableNotFound, @@ -492,7 +491,7 @@ def _get_tables(self, include_nested: bool = False, include_through: bool = Fals if include_through: yield from self.through_tables - @lru_cache() # type: ignore[misc] + @cached_method() # type: ignore[misc] def get_table_by_id( self, table_id: str, include_nested: bool = True, include_through: bool = True ) -> DatasetTableSchema: @@ -931,7 +930,7 @@ def get_db_fields(self) -> Iterator[DatasetFieldSchema]: continue yield field - @lru_cache() # type: ignore[misc] + @cached_method() # type: ignore[misc] def get_field_by_id(self, field_id: str) -> DatasetFieldSchema: """Get a fields based on the ids of the field.""" for field_schema in self.fields: @@ -940,7 +939,7 @@ def get_field_by_id(self, field_id: str) -> DatasetFieldSchema: raise DatasetFieldNotFound(f"Field '{field_id}' does not exist in table '{self.id}'.") - @lru_cache() # type: ignore[misc] + @cached_method() # type: ignore[misc] def get_additional_relation_by_id(self, relation_id: str) -> AdditionalRelationSchema: """Get the reverse relation based on the ids of the relation.""" for additional_relation in self.additional_relations: @@ -1631,7 +1630,7 @@ def field_items(self) -> Json | None: """Return the item definition for an array type.""" return self.get("items", {}) if self.is_array else None - @lru_cache() # type: ignore[misc] + @cached_method() # type: ignore[misc] def get_field_by_id(self, field_id: str) -> DatasetFieldSchema: """Finds and returns the subfield with the given id.