diff options
| author | mike bayer <mike_mp@zzzcomputing.com> | 2022-11-11 21:30:43 +0000 |
|---|---|---|
| committer | Gerrit Code Review <gerrit@ci3.zzzcomputing.com> | 2022-11-11 21:30:43 +0000 |
| commit | 6b140afe7db3456c995f50bacaca69112db72d70 (patch) | |
| tree | 6163a6e3f4a5dd78073a82151c96e351a541c6a5 /lib/sqlalchemy | |
| parent | 1dd0f23e8d74aa7edc8dd309093a95171e2e8f09 (diff) | |
| parent | 1d8833a9c1ada64cfc716109a8836b32cb8a9bd6 (diff) | |
| download | sqlalchemy-6b140afe7db3456c995f50bacaca69112db72d70.tar.gz | |
Merge "ensure anon_map is passed for most annotated traversals" into main
Diffstat (limited to 'lib/sqlalchemy')
| -rw-r--r-- | lib/sqlalchemy/sql/annotation.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/cache_key.py | 17 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/elements.py | 27 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/schema.py | 11 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/type_api.py | 4 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/__init__.py | 1 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/assertions.py | 11 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/util.py | 63 | ||||
| -rw-r--r-- | lib/sqlalchemy/util/__init__.py | 1 | ||||
| -rw-r--r-- | lib/sqlalchemy/util/compat.py | 1 |
10 files changed, 134 insertions, 10 deletions
diff --git a/lib/sqlalchemy/sql/annotation.py b/lib/sqlalchemy/sql/annotation.py index 262048bd1..43ca84abb 100644 --- a/lib/sqlalchemy/sql/annotation.py +++ b/lib/sqlalchemy/sql/annotation.py @@ -94,12 +94,18 @@ class SupportsAnnotations(ExternallyTraversible): @util.memoized_property def _annotations_cache_key(self) -> Tuple[Any, ...]: anon_map_ = anon_map() + + return self._gen_annotations_cache_key(anon_map_) + + def _gen_annotations_cache_key( + self, anon_map: anon_map + ) -> Tuple[Any, ...]: return ( "_annotations", tuple( ( key, - value._gen_cache_key(anon_map_, []) + value._gen_cache_key(anon_map, []) if isinstance(value, HasCacheKey) else value, ) diff --git a/lib/sqlalchemy/sql/cache_key.py b/lib/sqlalchemy/sql/cache_key.py index 88148285c..39d09d3ab 100644 --- a/lib/sqlalchemy/sql/cache_key.py +++ b/lib/sqlalchemy/sql/cache_key.py @@ -297,12 +297,17 @@ class HasCacheKey: else None, ) elif meth is InternalTraversal.dp_annotations_key: - # obj is here is the _annotations dict. however, we - # want to use the memoized cache key version of it. for - # Columns, this should be long lived. For select() - # statements, not so much, but they usually won't have - # annotations. - result += self._annotations_cache_key # type: ignore + # obj is here is the _annotations dict. Table uses + # a memoized version of it. however in other cases, + # we generate it given anon_map as we may be from a + # Join, Aliased, etc. + # see #8790 + + if self._gen_static_annotations_cache_key: # type: ignore # noqa: E501 + result += self._annotations_cache_key # type: ignore # noqa: E501 + else: + result += self._gen_annotations_cache_key(anon_map) # type: ignore # noqa: E501 + elif ( meth is InternalTraversal.dp_clauseelement_list or meth is InternalTraversal.dp_clauseelement_tuple diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 3b70e8d4e..6a5aa7db9 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -334,6 +334,7 @@ class ClauseElement( _is_column_element = False _is_keyed_column_element = False _is_table = False + _gen_static_annotations_cache_key = False _is_textual = False _is_from_clause = False _is_returns_rows = False @@ -3224,7 +3225,7 @@ class Cast(WrapsColumnExpression[_T]): _traverse_internals: _TraverseInternalsType = [ ("clause", InternalTraversal.dp_clauseelement), - ("typeclause", InternalTraversal.dp_clauseelement), + ("type", InternalTraversal.dp_type), ] clause: ColumnElement[Any] @@ -3631,7 +3632,20 @@ class BinaryExpression(OperatorExpression[_T]): ( "type", InternalTraversal.dp_type, - ), # affects JSON CAST operators + ), + ] + + _cache_key_traversal = [ + ("left", InternalTraversal.dp_clauseelement), + ("right", InternalTraversal.dp_clauseelement), + ("operator", InternalTraversal.dp_operator), + ("modifiers", InternalTraversal.dp_plain_dict), + # "type" affects JSON CAST operators, so while redundant in most cases, + # is needed for that one + ( + "type", + InternalTraversal.dp_type, + ), ] _is_implicitly_boolean = True @@ -3816,6 +3830,10 @@ class Grouping(GroupedElement, ColumnElement[_T]): ("type", InternalTraversal.dp_type), ] + _cache_key_traversal = [ + ("element", InternalTraversal.dp_clauseelement), + ] + element: Union[TextClause, ClauseList, ColumnElement[_T]] def __init__( @@ -4322,6 +4340,11 @@ class Label(roles.LabeledColumnExprRole[_T], NamedColumn[_T]): ("_element", InternalTraversal.dp_clauseelement), ] + _cache_key_traversal = [ + ("name", InternalTraversal.dp_anon_name), + ("_element", InternalTraversal.dp_clauseelement), + ] + _element: ColumnElement[_T] name: str diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index 36c33868a..dde5cd372 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -2023,6 +2023,17 @@ class Column(DialectKWArgs, SchemaItem, ColumnClause[_T]): identity: Optional[Identity] + @util.memoized_property + def _gen_static_annotations_cache_key(self) -> bool: # type: ignore + """special attribute used by cache key gen, if true, we will + use a static cache key for the annotations dictionary, else we + will generate a new cache key for annotations each time. + + Added for #8790 + + """ + return self.table is not None and self.table._is_table + def _extra_kwargs(self, **kwargs: Any) -> None: self._validate_dialect_kwargs(kwargs) diff --git a/lib/sqlalchemy/sql/type_api.py b/lib/sqlalchemy/sql/type_api.py index 90320701e..cd57ee3b6 100644 --- a/lib/sqlalchemy/sql/type_api.py +++ b/lib/sqlalchemy/sql/type_api.py @@ -940,7 +940,9 @@ class TypeEngine(Visitable, Generic[_T]): else self.__dict__[k], ) for k in names - if k in self.__dict__ and not k.startswith("_") + if k in self.__dict__ + and not k.startswith("_") + and self.__dict__[k] is not None ) @overload diff --git a/lib/sqlalchemy/testing/__init__.py b/lib/sqlalchemy/testing/__init__.py index 0c83cb469..76445a444 100644 --- a/lib/sqlalchemy/testing/__init__.py +++ b/lib/sqlalchemy/testing/__init__.py @@ -31,6 +31,7 @@ from .assertions import expect_raises from .assertions import expect_raises_message from .assertions import expect_warnings from .assertions import in_ +from .assertions import int_within_variance from .assertions import is_ from .assertions import is_false from .assertions import is_instance_of diff --git a/lib/sqlalchemy/testing/assertions.py b/lib/sqlalchemy/testing/assertions.py index 44e7e892f..321c05b44 100644 --- a/lib/sqlalchemy/testing/assertions.py +++ b/lib/sqlalchemy/testing/assertions.py @@ -236,6 +236,17 @@ def _assert_no_stray_pool_connections(): engines.testing_reaper.assert_all_closed() +def int_within_variance(expected, received, variance): + deviance = int(expected * variance) + assert ( + abs(received - expected) < deviance + ), "Given int value %s is not within %d%% of expected value %s" % ( + received, + variance * 100, + expected, + ) + + def eq_regex(a, b, msg=None): assert re.match(b, a), msg or "%r !~ %r" % (a, b) diff --git a/lib/sqlalchemy/testing/util.py b/lib/sqlalchemy/testing/util.py index 6fd42af70..74b1ca992 100644 --- a/lib/sqlalchemy/testing/util.py +++ b/lib/sqlalchemy/testing/util.py @@ -9,10 +9,13 @@ from __future__ import annotations +from collections import deque import decimal import gc +from itertools import chain import random import sys +from sys import getsizeof import types from . import config @@ -459,3 +462,63 @@ def teardown_events(event_cls): event_cls._clear() return decorate + + +def total_size(o): + """Returns the approximate memory footprint an object and all of its + contents. + + source: https://code.activestate.com/recipes/577504/ + + + """ + + def dict_handler(d): + return chain.from_iterable(d.items()) + + all_handlers = { + tuple: iter, + list: iter, + deque: iter, + dict: dict_handler, + set: iter, + frozenset: iter, + } + seen = set() # track which object id's have already been seen + default_size = getsizeof(0) # estimate sizeof object without __sizeof__ + + def sizeof(o): + if id(o) in seen: # do not double count the same object + return 0 + seen.add(id(o)) + s = getsizeof(o, default_size) + + for typ, handler in all_handlers.items(): + if isinstance(o, typ): + s += sum(map(sizeof, handler(o))) + break + return s + + return sizeof(o) + + +def count_cache_key_tuples(tup): + """given a cache key tuple, counts how many instances of actual + tuples are found. + + used to alert large jumps in cache key complexity. + + """ + stack = [tup] + + sentinel = object() + num_elements = 0 + + while stack: + elem = stack.pop(0) + if elem is sentinel: + num_elements += 1 + elif isinstance(elem, tuple): + if elem: + stack = list(elem) + [sentinel] + stack + return num_elements diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py index 4952cb501..bb4642a4f 100644 --- a/lib/sqlalchemy/util/__init__.py +++ b/lib/sqlalchemy/util/__init__.py @@ -62,6 +62,7 @@ from .compat import local_dataclass_fields as local_dataclass_fields from .compat import osx as osx from .compat import py310 as py310 from .compat import py311 as py311 +from .compat import py312 as py312 from .compat import py38 as py38 from .compat import py39 as py39 from .compat import pypy as pypy diff --git a/lib/sqlalchemy/util/compat.py b/lib/sqlalchemy/util/compat.py index cda5ab6c1..2899b4258 100644 --- a/lib/sqlalchemy/util/compat.py +++ b/lib/sqlalchemy/util/compat.py @@ -30,6 +30,7 @@ from typing import Tuple from typing import Type +py312 = sys.version_info >= (3, 12) py311 = sys.version_info >= (3, 11) py310 = sys.version_info >= (3, 10) py39 = sys.version_info >= (3, 9) |
