From: Federico Caselli Date: Sat, 7 Mar 2020 18:17:07 +0000 (+0100) Subject: Simplified module pre-loading strategy and made it linter friendly X-Git-Tag: rel_1_4_0b1~477^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=eda6dbbf387def2063d1b6719b64b20f9e7f2ab4;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Simplified module pre-loading strategy and made it linter friendly Introduced a modules registry to register modules that should be lazily loaded in the package init. This ensures that they are in the system module cache, avoiding potential thread safety issues as when importing them directly in the function that uses them. The module registry is used to obtain these modules directly, ensuring that the all the lazily loaded modules are resolved at the proper time This replaces dependency_for decorator and the dependencies decorator logic, removing the need to pass the resolved modules as arguments of the decodated functions and removes possible errors caused by linters. Fixes: #4689 Fixes: #4656 Change-Id: I2e291eba4297867fc0ddb5d875b9f7af34751d01 --- diff --git a/doc/build/orm/internals.rst b/doc/build/orm/internals.rst index 2658e24eee..fe35178a69 100644 --- a/doc/build/orm/internals.rst +++ b/doc/build/orm/internals.rst @@ -21,7 +21,7 @@ sections, are listed here. .. autoclass:: sqlalchemy.orm.properties.ColumnProperty :members: -.. autoclass:: sqlalchemy.orm.properties.ComparableProperty +.. autoclass:: sqlalchemy.orm.descriptor_props.ComparableProperty :members: .. autoclass:: sqlalchemy.orm.descriptor_props.CompositeProperty @@ -84,7 +84,7 @@ sections, are listed here. :members: :inherited-members: -.. autoclass:: sqlalchemy.orm.properties.RelationshipProperty +.. autoclass:: sqlalchemy.orm.relationships.RelationshipProperty :members: :inherited-members: diff --git a/examples/versioned_history/history_meta.py b/examples/versioned_history/history_meta.py index eb824bd7b6..f2b3f8118d 100644 --- a/examples/versioned_history/history_meta.py +++ b/examples/versioned_history/history_meta.py @@ -14,7 +14,7 @@ from sqlalchemy.orm import attributes from sqlalchemy.orm import mapper from sqlalchemy.orm import object_mapper from sqlalchemy.orm.exc import UnmappedColumnError -from sqlalchemy.orm.properties import RelationshipProperty +from sqlalchemy.orm.relationships import RelationshipProperty def col_references_table(col, table): diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py index 2d67db24cf..0f18aba331 100644 --- a/lib/sqlalchemy/__init__.py +++ b/lib/sqlalchemy/__init__.py @@ -139,7 +139,7 @@ def __go(lcls): if not (name.startswith("_") or _inspect.ismodule(obj)) ) - _sa_util.dependencies.resolve_all("sqlalchemy") + _sa_util.preloaded.import_prefix("sqlalchemy") __go(locals()) diff --git a/lib/sqlalchemy/ext/__init__.py b/lib/sqlalchemy/ext/__init__.py index 247301b94f..1f842fc2a9 100644 --- a/lib/sqlalchemy/ext/__init__.py +++ b/lib/sqlalchemy/ext/__init__.py @@ -8,4 +8,4 @@ from .. import util as _sa_util -_sa_util.dependencies.resolve_all("sqlalchemy.ext") +_sa_util.preloaded.import_prefix("sqlalchemy.ext") diff --git a/lib/sqlalchemy/ext/declarative/api.py b/lib/sqlalchemy/ext/declarative/api.py index c8695b7bff..ca7d3a022b 100644 --- a/lib/sqlalchemy/ext/declarative/api.py +++ b/lib/sqlalchemy/ext/declarative/api.py @@ -23,7 +23,7 @@ from ...orm import attributes from ...orm import comparable_property from ...orm import exc as orm_exc from ...orm import interfaces -from ...orm import properties +from ...orm import relationships from ...orm import synonym as _orm_synonym from ...orm.base import _inspect_mapped_class from ...orm.base import _mapper_or_none @@ -761,7 +761,7 @@ class DeferredReflection(object): metadata = mapper.class_.metadata for rel in mapper._props.values(): if ( - isinstance(rel, properties.RelationshipProperty) + isinstance(rel, relationships.RelationshipProperty) and rel.secondary is not None ): if isinstance(rel.secondary, Table): diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py index 51ba35b4b7..314e96cf10 100644 --- a/lib/sqlalchemy/ext/declarative/base.py +++ b/lib/sqlalchemy/ext/declarative/base.py @@ -22,9 +22,9 @@ from ...orm import synonym from ...orm.attributes import QueryableAttribute from ...orm.base import _is_mapped_class from ...orm.base import InspectionAttr +from ...orm.descriptor_props import CompositeProperty from ...orm.interfaces import MapperProperty from ...orm.properties import ColumnProperty -from ...orm.properties import CompositeProperty from ...schema import Column from ...schema import Table from ...sql import expression diff --git a/lib/sqlalchemy/ext/declarative/clsregistry.py b/lib/sqlalchemy/ext/declarative/clsregistry.py index 93e643cf5c..71594aae7c 100644 --- a/lib/sqlalchemy/ext/declarative/clsregistry.py +++ b/lib/sqlalchemy/ext/declarative/clsregistry.py @@ -16,10 +16,10 @@ from ... import exc from ... import inspection from ... import util from ...orm import class_mapper +from ...orm import ColumnProperty from ...orm import interfaces -from ...orm.properties import ColumnProperty -from ...orm.properties import RelationshipProperty -from ...orm.properties import SynonymProperty +from ...orm import RelationshipProperty +from ...orm import SynonymProperty from ...schema import _get_table_key diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 307f55ad0a..b5ebb93e00 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -276,8 +276,8 @@ def __go(lcls): if not (name.startswith("_") or _inspect.ismodule(obj)) ) - _sa_util.dependencies.resolve_all("sqlalchemy.orm") - _sa_util.dependencies.resolve_all("sqlalchemy.ext") + _sa_util.preloaded.import_prefix("sqlalchemy.orm") + _sa_util.preloaded.import_prefix("sqlalchemy.ext") __go(locals()) diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index bebf72a9d0..c2067c228d 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -12,7 +12,6 @@ as actively in the load/persist ORM loop. """ from . import attributes -from . import properties from . import query from .interfaces import MapperProperty from .interfaces import PropComparator @@ -85,7 +84,6 @@ class DescriptorProperty(MapperProperty): mapper.class_manager.instrument_attribute(self.key, proxy_attr) -@util.langhelpers.dependency_for("sqlalchemy.orm.properties", add_to_all=True) class CompositeProperty(DescriptorProperty): """Defines a "composite" mapped attribute, representing a collection of columns as one attribute. @@ -463,7 +461,6 @@ class CompositeProperty(DescriptorProperty): return str(self.parent.class_.__name__) + "." + self.key -@util.langhelpers.dependency_for("sqlalchemy.orm.properties", add_to_all=True) class ConcreteInheritedProperty(DescriptorProperty): """A 'do nothing' :class:`.MapperProperty` that disables an attribute on a concrete subclass that is only present @@ -517,7 +514,6 @@ class ConcreteInheritedProperty(DescriptorProperty): self.descriptor = NoninheritedConcreteProp() -@util.langhelpers.dependency_for("sqlalchemy.orm.properties", add_to_all=True) class SynonymProperty(DescriptorProperty): def __init__( self, @@ -672,7 +668,10 @@ class SynonymProperty(DescriptorProperty): attr = getattr(self.parent.class_, self.name) return attr.impl.get_history(*arg, **kw) + @util.preload_module("sqlalchemy.orm.properties") def set_parent(self, parent, init): + properties = util.preloaded.orm_properties + if self.map_column: # implement the 'map_column' option. if self.key not in parent.persist_selectable.c: @@ -708,7 +707,6 @@ class SynonymProperty(DescriptorProperty): self.parent = parent -@util.langhelpers.dependency_for("sqlalchemy.orm.properties", add_to_all=True) @util.deprecated_cls( "0.7", ":func:`.comparable_property` is deprecated and will be removed in a " diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index a8d2731db0..2a3ef54dd0 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -17,7 +17,7 @@ from . import exc as orm_exc from . import interfaces from . import object_mapper from . import object_session -from . import properties +from . import relationships from . import strategies from . import util as orm_util from .query import Query @@ -27,7 +27,7 @@ from .. import util @log.class_logger -@properties.RelationshipProperty.strategy_for(lazy="dynamic") +@relationships.RelationshipProperty.strategy_for(lazy="dynamic") class DynaLoader(strategies.AbstractRelationshipLoader): def init_class_attribute(self, mapper): self.is_class_level = True diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py index f0db1d86f8..4a50f64644 100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@ -183,8 +183,10 @@ class InstanceEvents(event.Events): _InstanceEventsHold.populate(class_, classmanager) @classmethod - @util.dependencies("sqlalchemy.orm") - def _accept_with(cls, orm, target): + @util.preload_module("sqlalchemy.orm") + def _accept_with(cls, target): + orm = util.preloaded.orm + if isinstance(target, instrumentation.ClassManager): return target elif isinstance(target, mapperlib.Mapper): @@ -665,8 +667,10 @@ class MapperEvents(event.Events): _MapperEventsHold.populate(class_, mapper) @classmethod - @util.dependencies("sqlalchemy.orm") - def _accept_with(cls, orm, target): + @util.preload_module("sqlalchemy.orm") + def _accept_with(cls, target): + orm = util.preloaded.orm + if target is orm.mapper: return mapperlib.Mapper elif isinstance(target, type): diff --git a/lib/sqlalchemy/orm/exc.py b/lib/sqlalchemy/orm/exc.py index f009b9c0b1..0c25654876 100644 --- a/lib/sqlalchemy/orm/exc.py +++ b/lib/sqlalchemy/orm/exc.py @@ -67,8 +67,10 @@ class DetachedInstanceError(sa_exc.SQLAlchemyError): class UnmappedInstanceError(UnmappedError): """An mapping operation was requested for an unknown instance.""" - @util.dependencies("sqlalchemy.orm.base") - def __init__(self, base, obj, msg=None): + @util.preload_module("sqlalchemy.orm.base") + def __init__(self, obj, msg=None): + base = util.preloaded.orm_base + if not msg: try: base.class_mapper(type(obj)) @@ -124,8 +126,10 @@ class ObjectDeletedError(sa_exc.InvalidRequestError): """ - @util.dependencies("sqlalchemy.orm.base") - def __init__(self, base, state, msg=None): + @util.preload_module("sqlalchemy.orm.base") + def __init__(self, state, msg=None): + base = util.preloaded.orm_base + if not msg: msg = ( "Instance '%s' has been deleted, or its " @@ -192,8 +196,10 @@ def _safe_cls_name(cls): return cls_name -@util.dependencies("sqlalchemy.orm.base") -def _default_unmapped(base, cls): +@util.preload_module("sqlalchemy.orm.base") +def _default_unmapped(cls): + base = util.preloaded.orm_base + try: mappers = base.manager_of_class(cls).mappers except NO_STATE: diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py index d943ebb190..c3d4773cd3 100644 --- a/lib/sqlalchemy/orm/loading.py +++ b/lib/sqlalchemy/orm/loading.py @@ -104,9 +104,10 @@ def instances(query, cursor, context): cursor.close() -@util.dependencies("sqlalchemy.orm.query") -def merge_result(querylib, query, iterator, load=True): +@util.preload_module("sqlalchemy.orm.query") +def merge_result(query, iterator, load=True): """Merge a result into this :class:`.Query` object's Session.""" + querylib = util.preloaded.orm_query session = query.session if load: diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 91e3251e2c..5619d2e16e 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1659,7 +1659,10 @@ class Mapper(sql_base.HasCacheKey, InspectionAttr): def _prop_set(self): return frozenset(self._props.values()) + @util.preload_module("sqlalchemy.orm.descriptor_props") def _adapt_inherited_property(self, key, prop, init): + descriptor_props = util.preloaded.orm_descriptor_props + if not self.concrete: self._configure_property(key, prop, init=False, setparent=False) elif key not in self._props: @@ -1680,12 +1683,14 @@ class Mapper(sql_base.HasCacheKey, InspectionAttr): ): self._configure_property( key, - properties.ConcreteInheritedProperty(), + descriptor_props.ConcreteInheritedProperty(), init=init, setparent=True, ) + @util.preload_module("sqlalchemy.orm.descriptor_props") def _configure_property(self, key, prop, init=True, setparent=True): + descriptor_props = util.preloaded.orm_descriptor_props self._log("_configure_property(%s, %s)", key, prop.__class__.__name__) if not isinstance(prop, MapperProperty): @@ -1769,7 +1774,7 @@ class Mapper(sql_base.HasCacheKey, InspectionAttr): self._props[key], ( properties.ColumnProperty, - properties.ConcreteInheritedProperty, + descriptor_props.ConcreteInheritedProperty, ), ) ): @@ -1796,10 +1801,11 @@ class Mapper(sql_base.HasCacheKey, InspectionAttr): if self.configured: self._expire_memoizations() + @util.preload_module("sqlalchemy.orm.descriptor_props") def _property_from_column(self, key, prop): - """generate/update a :class:`.ColumnProprerty` given a + """generate/update a :class:`.ColumnProperty` given a :class:`.Column` object. """ - + descriptor_props = util.preloaded.orm_descriptor_props # we were passed a Column or a list of Columns; # generate a properties.ColumnProperty columns = util.to_list(prop) @@ -1841,7 +1847,7 @@ class Mapper(sql_base.HasCacheKey, InspectionAttr): ) return prop elif prop is None or isinstance( - prop, properties.ConcreteInheritedProperty + prop, descriptor_props.ConcreteInheritedProperty ): mapped_column = [] for c in columns: @@ -2374,6 +2380,7 @@ class Mapper(sql_base.HasCacheKey, InspectionAttr): ) @_memoized_configured_property + @util.preload_module("sqlalchemy.orm.descriptor_props") def synonyms(self): """Return a namespace of all :class:`.SynonymProperty` properties maintained by this :class:`.Mapper`. @@ -2384,7 +2391,9 @@ class Mapper(sql_base.HasCacheKey, InspectionAttr): objects. """ - return self._filter_properties(properties.SynonymProperty) + descriptor_props = util.preloaded.orm_descriptor_props + + return self._filter_properties(descriptor_props.SynonymProperty) @_memoized_configured_property def column_attrs(self): @@ -2399,6 +2408,7 @@ class Mapper(sql_base.HasCacheKey, InspectionAttr): """ return self._filter_properties(properties.ColumnProperty) + @util.preload_module("sqlalchemy.orm.relationships") @_memoized_configured_property def relationships(self): """A namespace of all :class:`.RelationshipProperty` properties @@ -2422,9 +2432,12 @@ class Mapper(sql_base.HasCacheKey, InspectionAttr): objects. """ - return self._filter_properties(properties.RelationshipProperty) + return self._filter_properties( + util.preloaded.orm_relationships.RelationshipProperty + ) @_memoized_configured_property + @util.preload_module("sqlalchemy.orm.descriptor_props") def composites(self): """Return a namespace of all :class:`.CompositeProperty` properties maintained by this :class:`.Mapper`. @@ -2435,7 +2448,9 @@ class Mapper(sql_base.HasCacheKey, InspectionAttr): objects. """ - return self._filter_properties(properties.CompositeProperty) + return self._filter_properties( + util.preloaded.orm_descriptor_props.CompositeProperty + ) def _filter_properties(self, type_): if Mapper._new_mappers: @@ -2916,14 +2931,17 @@ class Mapper(sql_base.HasCacheKey, InspectionAttr): return None - @util.dependencies( + @util.preload_module( "sqlalchemy.ext.baked", "sqlalchemy.orm.strategy_options" ) - def _subclass_load_via_in(self, baked, strategy_options, entity): + def _subclass_load_via_in(self, entity): """Assemble a BakedQuery that can load the columns local to this subclass as a SELECT with IN. """ + strategy_options = util.preloaded.orm_strategy_options + baked = util.preloaded.ext_baked + assert self.inherits polymorphic_prop = self._columntoproperty[self.polymorphic_on] diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index 46c84d4bda..1460ae2089 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -1710,8 +1710,9 @@ class BulkUD(object): def _do_before_compile(self): raise NotImplementedError() - @util.dependencies("sqlalchemy.orm.query") - def _do_pre(self, querylib): + @util.preload_module("sqlalchemy.orm.query") + def _do_pre(self): + querylib = util.preloaded.orm_query query = self.query self.context = querylib.QueryContext(query) diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 6ad8606e31..aa9dd3274c 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -14,8 +14,13 @@ mapped attributes. from __future__ import absolute_import from . import attributes +from .descriptor_props import ComparableProperty +from .descriptor_props import CompositeProperty +from .descriptor_props import ConcreteInheritedProperty +from .descriptor_props import SynonymProperty from .interfaces import PropComparator from .interfaces import StrategizedProperty +from .relationships import RelationshipProperty from .util import _orm_full_deannotate from .. import log from .. import util @@ -23,7 +28,14 @@ from ..sql import coercions from ..sql import roles -__all__ = ["ColumnProperty"] +__all__ = [ + "ColumnProperty", + "ComparableProperty", + "CompositeProperty", + "ConcreteInheritedProperty", + "RelationshipProperty", + "SynonymProperty", +] @log.class_logger @@ -190,16 +202,20 @@ class ColumnProperty(StrategizedProperty): if self.raiseload: self.strategy_key += (("raiseload", True),) - @util.dependencies("sqlalchemy.orm.state", "sqlalchemy.orm.strategies") - def _memoized_attr__deferred_column_loader(self, state, strategies): + @util.preload_module("sqlalchemy.orm.state", "sqlalchemy.orm.strategies") + def _memoized_attr__deferred_column_loader(self): + state = util.preloaded.orm_state + strategies = util.preloaded.orm_strategies return state.InstanceState._instance_level_callable_processor( self.parent.class_manager, strategies.LoadDeferredColumns(self.key), self.key, ) - @util.dependencies("sqlalchemy.orm.state", "sqlalchemy.orm.strategies") - def _memoized_attr__raise_column_loader(self, state, strategies): + @util.preload_module("sqlalchemy.orm.state", "sqlalchemy.orm.strategies") + def _memoized_attr__raise_column_loader(self): + state = util.preloaded.orm_state + strategies = util.preloaded.orm_strategies return state.InstanceState._instance_level_callable_processor( self.parent.class_manager, strategies.LoadDeferredColumns(self.key, True), diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 830cede93d..796ebe3ac4 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -26,7 +26,6 @@ from . import exc as orm_exc from . import interfaces from . import loading from . import persistence -from . import properties from .base import _assertions from .base import _entity_descriptor from .base import _is_aliased_class @@ -1120,6 +1119,7 @@ class Query(Generative): """ self._invoke_all_eagers = value + @util.preload_module("sqlalchemy.orm.relationships") def with_parent(self, instance, property=None, from_entity=None): # noqa """Add filtering criterion that relates the given instance to a child object or collection, using its attribute state @@ -1146,6 +1146,7 @@ class Query(Generative): "zero" entity of the :class:`.Query` itself. """ + relationships = util.preloaded.orm_relationships if from_entity: entity_zero = inspect(from_entity) @@ -1157,7 +1158,7 @@ class Query(Generative): for prop in mapper.iterate_properties: if ( - isinstance(prop, properties.RelationshipProperty) + isinstance(prop, relationships.RelationshipProperty) and prop.mapper is entity_zero.mapper ): property = prop # noqa @@ -4089,7 +4090,7 @@ class Query(Generative): class LockmodeArg(ForUpdateArg): @classmethod - def parse_legacy_query(self, mode): + def parse_legacy_query(cls, mode): if mode in (None, False): return None diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index 2995baf5fc..f8437a00ff 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -20,8 +20,6 @@ import re import weakref from . import attributes -from . import dependency -from . import mapper as mapperlib from .base import state_str from .interfaces import MANYTOMANY from .interfaces import MANYTOONE @@ -96,7 +94,6 @@ def foreign(expr): @log.class_logger -@util.langhelpers.dependency_for("sqlalchemy.orm.properties", add_to_all=True) class RelationshipProperty(StrategizedProperty): """Describes an object property that holds a single item or list of items that correspond to a related database table. @@ -1514,7 +1511,9 @@ class RelationshipProperty(StrategizedProperty): return _orm_annotate(self.__negated_contains_or_equals(other)) @util.memoized_property + @util.preload_module("sqlalchemy.orm.mapper") def property(self): + mapperlib = util.preloaded.orm_mapper if mapperlib.Mapper._new_mappers: mapperlib.Mapper._configure_all() return self.prop @@ -1903,11 +1902,13 @@ class RelationshipProperty(StrategizedProperty): ) @util.memoized_property + @util.preload_module("sqlalchemy.orm.mapper") def entity(self): # type: () -> Union[AliasedInsp, mapperlib.Mapper] """Return the target mapped entity, which is an inspect() of the class or aliased class that is referred towards. """ + mapperlib = util.preloaded.orm_mapper if callable(self.argument) and not isinstance( self.argument, (type, mapperlib.Mapper) ): @@ -2045,10 +2046,11 @@ class RelationshipProperty(StrategizedProperty): self._calculated_foreign_keys = jc.foreign_key_columns self.secondary_synchronize_pairs = jc.secondary_synchronize_pairs + @util.preload_module("sqlalchemy.orm.mapper") def _check_conflicts(self): """Test that this relationship is legal, warn about inheritance conflicts.""" - + mapperlib = util.preloaded.orm_mapper if self.parent.non_primary and not mapperlib.class_mapper( self.parent.class_, configure=False ).has_property(self.key): @@ -2233,7 +2235,10 @@ class RelationshipProperty(StrategizedProperty): if self.back_populates: self._add_reverse_property(self.back_populates) + @util.preload_module("sqlalchemy.orm.dependency") def _post_init(self): + dependency = util.preloaded.orm_dependency + if self.uselist is None: self.uselist = self.direction is not MANYTOONE if not self.viewonly: @@ -3132,7 +3137,9 @@ class JoinCondition(object): _track_overlapping_sync_targets = weakref.WeakKeyDictionary() + @util.preload_module("sqlalchemy.orm.mapper") def _warn_for_conflicting_sync_targets(self): + mapperlib = util.preloaded.orm_mapper if not self.support_sync: return diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 74e5464835..b542f05065 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -71,14 +71,14 @@ class _SessionClassMethods(object): close_all_sessions() @classmethod - @util.dependencies("sqlalchemy.orm.util") - def identity_key(cls, orm_util, *args, **kwargs): + @util.preload_module("sqlalchemy.orm.util") + def identity_key(cls, *args, **kwargs): """Return an identity key. This is an alias of :func:`.util.identity_key`. """ - return orm_util.identity_key(*args, **kwargs) + return util.perload.orm_util.identity_key(*args, **kwargs) @classmethod def object_session(cls, instance): diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index 41f878b771..91bf57ab92 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -227,11 +227,11 @@ class InstanceState(interfaces.InspectionAttrInfo): return self.key is not None and not self._attached @property - @util.dependencies("sqlalchemy.orm.session") - def _attached(self, sessionlib): + @util.preload_module("sqlalchemy.orm.session") + def _attached(self): return ( self.session_id is not None - and self.session_id in sessionlib._sessions + and self.session_id in util.preloaded.orm_session._sessions ) def _track_last_known_value(self, key): @@ -247,8 +247,8 @@ class InstanceState(interfaces.InspectionAttrInfo): self._last_known_values[key] = NO_VALUE @property - @util.dependencies("sqlalchemy.orm.session") - def session(self, sessionlib): + @util.preload_module("sqlalchemy.orm.session") + def session(self): """Return the owning :class:`.Session` for this instance, or ``None`` if none available. @@ -260,7 +260,7 @@ class InstanceState(interfaces.InspectionAttrInfo): fully detached under normal circumstances. """ - return sessionlib._state_session(self) + return util.preloaded.orm_session._state_session(self) @property def object(self): diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 9abb5be408..7d05a37f5c 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -18,6 +18,7 @@ from . import interfaces from . import loading from . import properties from . import query +from . import relationships from . import unitofwork from . import util as orm_util from .base import _DEFER_FOR_STATE @@ -486,7 +487,7 @@ class AbstractRelationshipLoader(LoaderStrategy): @log.class_logger -@properties.RelationshipProperty.strategy_for(do_nothing=True) +@relationships.RelationshipProperty.strategy_for(do_nothing=True) class DoNothingLoader(LoaderStrategy): """Relationship loader that makes no change to the object's state. @@ -498,8 +499,8 @@ class DoNothingLoader(LoaderStrategy): @log.class_logger -@properties.RelationshipProperty.strategy_for(lazy="noload") -@properties.RelationshipProperty.strategy_for(lazy=None) +@relationships.RelationshipProperty.strategy_for(lazy="noload") +@relationships.RelationshipProperty.strategy_for(lazy=None) class NoLoader(AbstractRelationshipLoader): """Provide loading behavior for a :class:`.RelationshipProperty` with "lazy=None". @@ -531,11 +532,11 @@ class NoLoader(AbstractRelationshipLoader): @log.class_logger -@properties.RelationshipProperty.strategy_for(lazy=True) -@properties.RelationshipProperty.strategy_for(lazy="select") -@properties.RelationshipProperty.strategy_for(lazy="raise") -@properties.RelationshipProperty.strategy_for(lazy="raise_on_sql") -@properties.RelationshipProperty.strategy_for(lazy="baked_select") +@relationships.RelationshipProperty.strategy_for(lazy=True) +@relationships.RelationshipProperty.strategy_for(lazy="select") +@relationships.RelationshipProperty.strategy_for(lazy="raise") +@relationships.RelationshipProperty.strategy_for(lazy="raise_on_sql") +@relationships.RelationshipProperty.strategy_for(lazy="baked_select") class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): """Provide loading behavior for a :class:`.RelationshipProperty` with "lazy=True", that is loads when first accessed. @@ -799,14 +800,12 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): for pk in self.mapper.primary_key ] - @util.dependencies("sqlalchemy.ext.baked") - def _memoized_attr__bakery(self, baked): - return baked.bakery(size=50) + @util.preload_module("sqlalchemy.ext.baked") + def _memoized_attr__bakery(self): + return util.preloaded.ext_baked.bakery(size=50) - @util.dependencies("sqlalchemy.orm.strategy_options") - def _emit_lazyload( - self, strategy_options, session, state, primary_key_identity, passive - ): + @util.preload_module("sqlalchemy.orm.strategy_options") + def _emit_lazyload(self, session, state, primary_key_identity, passive): # emit lazy load now using BakedQuery, to cut way down on the overhead # of generating queries. # there are two big things we are trying to guard against here: @@ -830,6 +829,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): # lazy loaders. Currently the LRU cache is local to the LazyLoader, # however add ourselves to the initial cache key just to future # proof in case it moves + strategy_options = util.preloaded.orm_strategy_options q = self._bakery(lambda session: session.query(self.entity), self) q.add_criteria( @@ -1008,7 +1008,7 @@ class PostLoader(AbstractRelationshipLoader): ) -@properties.RelationshipProperty.strategy_for(lazy="immediate") +@relationships.RelationshipProperty.strategy_for(lazy="immediate") class ImmediateLoader(PostLoader): __slots__ = () @@ -1040,7 +1040,7 @@ class ImmediateLoader(PostLoader): @log.class_logger -@properties.RelationshipProperty.strategy_for(lazy="subquery") +@relationships.RelationshipProperty.strategy_for(lazy="subquery") class SubqueryLoader(PostLoader): __slots__ = ("join_depth",) @@ -1492,8 +1492,8 @@ class SubqueryLoader(PostLoader): @log.class_logger -@properties.RelationshipProperty.strategy_for(lazy="joined") -@properties.RelationshipProperty.strategy_for(lazy=False) +@relationships.RelationshipProperty.strategy_for(lazy="joined") +@relationships.RelationshipProperty.strategy_for(lazy=False) class JoinedLoader(AbstractRelationshipLoader): """Provide loading behavior for a :class:`.RelationshipProperty` using joined eager loading. @@ -2144,7 +2144,7 @@ class JoinedLoader(AbstractRelationshipLoader): @log.class_logger -@properties.RelationshipProperty.strategy_for(lazy="selectin") +@relationships.RelationshipProperty.strategy_for(lazy="selectin") class SelectInLoader(PostLoader, util.MemoizedSlots): __slots__ = ( "join_depth", @@ -2255,9 +2255,9 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): (("lazy", "select"),) ).init_class_attribute(mapper) - @util.dependencies("sqlalchemy.ext.baked") - def _memoized_attr__bakery(self, baked): - return baked.bakery(size=50) + @util.preload_module("sqlalchemy.ext.baked") + def _memoized_attr__bakery(self): + return util.preloaded.ext_baked.bakery(size=50) def create_row_processor( self, context, path, loadopt, mapper, result, adapter, populators @@ -2313,11 +2313,9 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): effective_entity, ) - @util.dependencies("sqlalchemy.ext.baked") def _load_for_path( - self, baked, context, path, states, load_only, effective_entity + self, context, path, states, load_only, effective_entity ): - if load_only and self.key not in load_only: return diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py index 24e6fe205e..5a3f99e700 100644 --- a/lib/sqlalchemy/orm/unitofwork.py +++ b/lib/sqlalchemy/orm/unitofwork.py @@ -15,7 +15,6 @@ organizes them in order of dependency, and executes. from . import attributes from . import exc as orm_exc -from . import persistence from . import util as orm_util from .. import event from .. import util @@ -568,7 +567,9 @@ class PostUpdateAll(PostSortRec): self.mapper = mapper self.isdelete = isdelete + @util.preload_module("sqlalchemy.orm.persistence") def execute(self, uow): + persistence = util.preloaded.orm_persistence states, cols = uow.post_update_states[self.mapper] states = [s for s in states if uow.states[s][0] == self.isdelete] @@ -582,8 +583,9 @@ class SaveUpdateAll(PostSortRec): self.mapper = mapper assert mapper is mapper.base_mapper + @util.preload_module("sqlalchemy.orm.persistence") def execute(self, uow): - persistence.save_obj( + util.preloaded.orm_persistence.save_obj( self.mapper, uow.states_for_mapper_hierarchy(self.mapper, False, False), uow, @@ -617,8 +619,9 @@ class DeleteAll(PostSortRec): self.mapper = mapper assert mapper is mapper.base_mapper + @util.preload_module("sqlalchemy.orm.persistence") def execute(self, uow): - persistence.delete_obj( + util.preloaded.orm_persistence.delete_obj( self.mapper, uow.states_for_mapper_hierarchy(self.mapper, True, False), uow, @@ -687,7 +690,9 @@ class SaveUpdateState(PostSortRec): self.state = state self.mapper = state.mapper.base_mapper + @util.preload_module("sqlalchemy.orm.persistence") def execute_aggregate(self, uow, recs): + persistence = util.preloaded.orm_persistence cls_ = self.__class__ mapper = self.mapper our_recs = [ @@ -712,7 +717,9 @@ class DeleteState(PostSortRec): self.state = state self.mapper = state.mapper.base_mapper + @util.preload_module("sqlalchemy.orm.persistence") def execute_aggregate(self, uow, recs): + persistence = util.preloaded.orm_persistence cls_ = self.__class__ mapper = self.mapper our_recs = [ diff --git a/lib/sqlalchemy/sql/__init__.py b/lib/sqlalchemy/sql/__init__.py index 488717041d..281b7d0f24 100644 --- a/lib/sqlalchemy/sql/__init__.py +++ b/lib/sqlalchemy/sql/__init__.py @@ -120,7 +120,7 @@ def __go(lcls): _prepare_annotations(FromClause, AnnotatedFromClause) _prepare_annotations(ClauseList, Annotated) - _sa_util.dependencies.resolve_all("sqlalchemy.sql") + _sa_util.preloaded.import_prefix("sqlalchemy.sql") from . import naming # noqa diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py index 89839ea28d..b61c7dc5e1 100644 --- a/lib/sqlalchemy/sql/base.py +++ b/lib/sqlalchemy/sql/base.py @@ -209,6 +209,14 @@ class _DialectArgDict(util.collections_abc.MutableMapping): del self._non_defaults[key] +@util.preload_module("sqlalchemy.dialects") +def _kw_reg_for_dialect(dialect_name): + dialect_cls = util.preloaded.dialects.registry.load(dialect_name) + if dialect_cls.construct_arguments is None: + return None + return dict(dialect_cls.construct_arguments) + + class DialectKWArgs(object): """Establish the ability for a class to have dialect-specific arguments with defaults and constructor validation. @@ -307,13 +315,6 @@ class DialectKWArgs(object): """A synonym for :attr:`.DialectKWArgs.dialect_kwargs`.""" return self.dialect_kwargs - @util.dependencies("sqlalchemy.dialects") - def _kw_reg_for_dialect(dialects, dialect_name): - dialect_cls = dialects.registry.load(dialect_name) - if dialect_cls.construct_arguments is None: - return None - return dict(dialect_cls.construct_arguments) - _kw_registry = util.PopulateDict(_kw_reg_for_dialect) def _kw_reg_for_dialect_cls(self, dialect_name): diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 3ebcf24b03..b37c462160 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1022,9 +1022,10 @@ class SQLCompiler(Compiled): return expanded_state - @util.dependencies("sqlalchemy.engine.result") - def _create_result_map(self, result): + @util.preload_module("sqlalchemy.engine.result") + def _create_result_map(self): """utility method used for unit tests only.""" + result = util.preloaded.engine_result return result.CursorResultMetaData._create_description_match_map( self._result_columns ) @@ -4127,8 +4128,10 @@ class IdentifierPreparer(object): ident = self.quote_identifier(ident) return ident - @util.dependencies("sqlalchemy.sql.naming") - def format_constraint(self, naming, constraint, _alembic_quote=True): + @util.preload_module("sqlalchemy.sql.naming") + def format_constraint(self, constraint, _alembic_quote=True): + naming = util.preloaded.sql_naming + if isinstance(constraint.name, elements._defer_name): name = naming._constraint_name_for_table( constraint, constraint.table diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 47739a37df..bb68e8a7e2 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -422,8 +422,8 @@ class ClauseElement( return self - @util.dependencies("sqlalchemy.engine.default") - def compile(self, default, bind=None, dialect=None, **kw): + @util.preload_module("sqlalchemy.engine.default") + def compile(self, bind=None, dialect=None, **kw): """Compile this SQL expression. The return value is a :class:`~.Compiled` object. @@ -477,6 +477,7 @@ class ClauseElement( """ + default = util.preloaded.engine_default if not dialect: if bind: dialect = bind.dialect @@ -1782,8 +1783,8 @@ class TextClause( else: new_params[key] = existing._with_value(value) - @util.dependencies("sqlalchemy.sql.selectable") - def columns(self, selectable, *cols, **types): + @util.preload_module("sqlalchemy.sql.selectable") + def columns(self, *cols, **types): r"""Turn this :class:`.TextClause` object into a :class:`.TextualSelect` object that serves the same role as a SELECT statement. @@ -1888,6 +1889,7 @@ class TextClause( argument as it also indicates positional ordering. """ + selectable = util.preloaded.sql_selectable positional_input_cols = [ ColumnClause(col.key, types.pop(col.key)) if col.key in types diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index 4c627c4cc6..69f60ba246 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -2193,8 +2193,10 @@ class ColumnDefault(DefaultGenerator): ) @util.memoized_property - @util.dependencies("sqlalchemy.sql.sqltypes") - def _arg_is_typed(self, sqltypes): + @util.preload_module("sqlalchemy.sql.sqltypes") + def _arg_is_typed(self): + sqltypes = util.preloaded.sql_sqltypes + if self.is_clause_element: return not isinstance(self.arg.type, sqltypes.NullType) else: @@ -2440,14 +2442,16 @@ class Sequence(roles.StatementRole, DefaultGenerator): def is_clause_element(self): return False - @util.dependencies("sqlalchemy.sql.functions.func") - def next_value(self, func): + @util.preload_module("sqlalchemy.sql.functions") + def next_value(self): """Return a :class:`.next_value` function element which will render the appropriate increment function for this :class:`.Sequence` within any SQL expression. """ - return func.next_value(self, bind=self.bind) + return util.preloaded.sql_functions.func.next_value( + self, bind=self.bind + ) def _set_parent(self, column): super(Sequence, self)._set_parent(column) @@ -3925,10 +3929,10 @@ class MetaData(SchemaItem): """ return self._bind - @util.dependencies("sqlalchemy.engine.url") - def _bind_to(self, url, bind): + @util.preload_module("sqlalchemy.engine.url") + def _bind_to(self, bind): """Bind this MetaData to an Engine, Connection, string or URL.""" - + url = util.preloaded.engine_url if isinstance(bind, util.string_types + (url.URL,)): self._bind = sqlalchemy.create_engine(bind) else: @@ -4231,10 +4235,10 @@ class ThreadLocalMetaData(MetaData): return getattr(self.context, "_engine", None) - @util.dependencies("sqlalchemy.engine.url") - def _bind_to(self, url, bind): + @util.preload_module("sqlalchemy.engine.url") + def _bind_to(self, bind): """Bind to a Connectable in the caller's thread.""" - + url = util.preloaded.engine_url if isinstance(bind, util.string_types + (url.URL,)): try: self.context._engine = self.__engines[bind] diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index 965ac6e7fe..5536b27bc4 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -164,14 +164,13 @@ class Selectable(ReturnsRows): "deprecated, and will be removed in a future release. Similar " "functionality is available via the sqlalchemy.sql.visitors module.", ) - @util.dependencies("sqlalchemy.sql.util") - def replace_selectable(self, sqlutil, old, alias): + @util.preload_module("sqlalchemy.sql.util") + def replace_selectable(self, old, alias): """replace all occurrences of FromClause 'old' with the given Alias object, returning a copy of this :class:`.FromClause`. """ - - return sqlutil.ClauseAdapter(alias).traverse(self) + return util.preloaded.sql_util.ClauseAdapter(alias).traverse(self) def corresponding_column(self, column, require_embedded=False): """Given a :class:`.ColumnElement`, return the exported @@ -358,8 +357,8 @@ class FromClause(HasMemoized, roles.AnonymizedFromClauseRole, Selectable): ":class:`.functions.count` function available from the " ":attr:`.func` namespace.", ) - @util.dependencies("sqlalchemy.sql.functions") - def count(self, functions, whereclause=None, **params): + @util.preload_module("sqlalchemy.sql.functions") + def count(self, whereclause=None, **params): """return a SELECT COUNT generated against this :class:`.FromClause`. @@ -368,7 +367,7 @@ class FromClause(HasMemoized, roles.AnonymizedFromClauseRole, Selectable): :class:`.functions.count` """ - + functions = util.preloaded.sql_functions if self.primary_key: col = list(self.primary_key)[0] else: @@ -801,8 +800,9 @@ class Join(FromClause): def self_group(self, against=None): return FromGrouping(self) - @util.dependencies("sqlalchemy.sql.util") - def _populate_column_collection(self, sqlutil): + @util.preload_module("sqlalchemy.sql.util") + def _populate_column_collection(self): + sqlutil = util.preloaded.sql_util columns = [c for c in self.left.columns] + [ c for c in self.right.columns ] @@ -1033,8 +1033,8 @@ class Join(FromClause): def bind(self): return self.left.bind or self.right.bind - @util.dependencies("sqlalchemy.sql.util") - def alias(self, sqlutil, name=None, flat=False): + @util.preload_module("sqlalchemy.sql.util") + def alias(self, name=None, flat=False): r"""return an alias of this :class:`.Join`. The default behavior here is to first produce a SELECT @@ -1134,6 +1134,7 @@ class Join(FromClause): :func:`~.expression.alias` """ + sqlutil = util.preloaded.sql_util if flat: assert name is None, "Can't send name argument with flat" left_a, right_a = ( @@ -1458,8 +1459,9 @@ class TableSample(AliasedReturnsRows): self.seed = seed super(TableSample, self)._init(selectable, name=name) - @util.dependencies("sqlalchemy.sql.functions") - def _get_method(self, functions): + @util.preload_module("sqlalchemy.sql.functions") + def _get_method(self): + functions = util.preloaded.sql_functions if isinstance(self.sampling, functions.Function): return self.sampling else: @@ -1929,8 +1931,8 @@ class TableClause(Immutable, FromClause): self._columns.add(c) c.table = self - @util.dependencies("sqlalchemy.sql.dml") - def insert(self, dml, values=None, inline=False, **kwargs): + @util.preload_module("sqlalchemy.sql.dml") + def insert(self, values=None, inline=False, **kwargs): """Generate an :func:`.insert` construct against this :class:`.TableClause`. @@ -1941,13 +1943,12 @@ class TableClause(Immutable, FromClause): See :func:`.insert` for argument and usage information. """ + return util.preloaded.sql_dml.Insert( + self, values=values, inline=inline, **kwargs + ) - return dml.Insert(self, values=values, inline=inline, **kwargs) - - @util.dependencies("sqlalchemy.sql.dml") - def update( - self, dml, whereclause=None, values=None, inline=False, **kwargs - ): + @util.preload_module("sqlalchemy.sql.dml") + def update(self, whereclause=None, values=None, inline=False, **kwargs): """Generate an :func:`.update` construct against this :class:`.TableClause`. @@ -1958,8 +1959,7 @@ class TableClause(Immutable, FromClause): See :func:`.update` for argument and usage information. """ - - return dml.Update( + return util.preloaded.sql_dml.Update( self, whereclause=whereclause, values=values, @@ -1967,8 +1967,8 @@ class TableClause(Immutable, FromClause): **kwargs ) - @util.dependencies("sqlalchemy.sql.dml") - def delete(self, dml, whereclause=None, **kwargs): + @util.preload_module("sqlalchemy.sql.dml") + def delete(self, whereclause=None, **kwargs): """Generate a :func:`.delete` construct against this :class:`.TableClause`. @@ -1979,8 +1979,7 @@ class TableClause(Immutable, FromClause): See :func:`.delete` for argument and usage information. """ - - return dml.Delete(self, whereclause, **kwargs) + return util.preloaded.sql_dml.Delete(self, whereclause, **kwargs) @property def _from_objects(self): @@ -3864,8 +3863,8 @@ class Select( """ return self.add_columns(column) - @util.dependencies("sqlalchemy.sql.util") - def reduce_columns(self, sqlutil, only_synonyms=True): + @util.preload_module("sqlalchemy.sql.util") + def reduce_columns(self, only_synonyms=True): """Return a new :func`.select` construct with redundantly named, equivalently-valued columns removed from the columns clause. @@ -3887,7 +3886,7 @@ class Select( """ return self.with_only_columns( - sqlutil.reduce_columns( + util.preloaded.sql_util.reduce_columns( self.inner_columns, only_synonyms=only_synonyms, *(self._whereclause,) + tuple(self._from_obj) diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index 2d6b442991..3d69d11772 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -1539,8 +1539,9 @@ class Enum(Emulated, String, SchemaType): not self.native_enum or not compiler.dialect.supports_native_enum ) - @util.dependencies("sqlalchemy.sql.schema") - def _set_table(self, schema, column, table): + @util.preload_module("sqlalchemy.sql.schema") + def _set_table(self, column, table): + schema = util.preloaded.sql_schema SchemaType._set_table(self, column, table) if not self.create_constraint: @@ -1738,8 +1739,9 @@ class Boolean(Emulated, TypeEngine, SchemaType): and compiler.dialect.non_native_boolean_check_constraint ) - @util.dependencies("sqlalchemy.sql.schema") - def _set_table(self, schema, column, table): + @util.preload_module("sqlalchemy.sql.schema") + def _set_table(self, column, table): + schema = util.preloaded.sql_schema if not self.create_constraint: return @@ -2228,8 +2230,7 @@ class JSON(Indexable, TypeEngine): class Comparator(Indexable.Comparator, Concatenable.Comparator): """Define comparison operations for :class:`.types.JSON`.""" - @util.dependencies("sqlalchemy.sql.default_comparator") - def _setup_getitem(self, default_comparator, index): + def _setup_getitem(self, index): if not isinstance(index, util.string_types) and isinstance( index, compat.collections_abc.Sequence ): @@ -2553,8 +2554,8 @@ class ARRAY(SchemaEventTarget, Indexable, Concatenable, TypeEngine): "ARRAY type; please use the dialect-specific ARRAY type" ) - @util.dependencies("sqlalchemy.sql.elements") - def any(self, elements, other, operator=None): + @util.preload_module("sqlalchemy.sql.elements") + def any(self, other, operator=None): """Return ``other operator ANY (array)`` clause. Argument places are switched, because ANY requires array @@ -2582,14 +2583,15 @@ class ARRAY(SchemaEventTarget, Indexable, Concatenable, TypeEngine): :meth:`.types.ARRAY.Comparator.all` """ + elements = util.preloaded.sql_elements operator = operator if operator else operators.eq return operator( coercions.expect(roles.ExpressionElementRole, other), elements.CollectionAggregate._create_any(self.expr), ) - @util.dependencies("sqlalchemy.sql.elements") - def all(self, elements, other, operator=None): + @util.preload_module("sqlalchemy.sql.elements") + def all(self, other, operator=None): """Return ``other operator ALL (array)`` clause. Argument places are switched, because ALL requires array @@ -2617,6 +2619,7 @@ class ARRAY(SchemaEventTarget, Indexable, Concatenable, TypeEngine): :meth:`.types.ARRAY.Comparator.any` """ + elements = util.preloaded.sql_elements operator = operator if operator else operators.eq return operator( coercions.expect(roles.ExpressionElementRole, other), diff --git a/lib/sqlalchemy/sql/traversals.py b/lib/sqlalchemy/sql/traversals.py index c29a04ee09..2a4f7ebb68 100644 --- a/lib/sqlalchemy/sql/traversals.py +++ b/lib/sqlalchemy/sql/traversals.py @@ -597,9 +597,9 @@ class _GetChildren(InternalTraversal): _get_children = _GetChildren() -@util.dependencies("sqlalchemy.sql.elements") -def _resolve_name_for_compare(elements, element, name, anon_map, **kw): - if isinstance(name, elements._anonymous_label): +@util.preload_module("sqlalchemy.sql.elements") +def _resolve_name_for_compare(element, name, anon_map, **kw): + if isinstance(name, util.preloaded.sql_elements._anonymous_label): name = name.apply_map(anon_map) return name diff --git a/lib/sqlalchemy/sql/type_api.py b/lib/sqlalchemy/sql/type_api.py index 739f961954..38189ec9d5 100644 --- a/lib/sqlalchemy/sql/type_api.py +++ b/lib/sqlalchemy/sql/type_api.py @@ -64,13 +64,15 @@ class TypeEngine(Traversible): self.expr = expr self.type = expr.type - @util.dependencies("sqlalchemy.sql.default_comparator") - def operate(self, default_comparator, op, *other, **kwargs): + @util.preload_module("sqlalchemy.sql.default_comparator") + def operate(self, op, *other, **kwargs): + default_comparator = util.preloaded.sql_default_comparator o = default_comparator.operator_lookup[op.__name__] return o[0](self.expr, op, *(other + o[1:]), **kwargs) - @util.dependencies("sqlalchemy.sql.default_comparator") - def reverse_operate(self, default_comparator, op, other, **kwargs): + @util.preload_module("sqlalchemy.sql.default_comparator") + def reverse_operate(self, op, other, **kwargs): + default_comparator = util.preloaded.sql_default_comparator o = default_comparator.operator_lookup[op.__name__] return o[0](self.expr, op, other, reverse=True, *o[1:], **kwargs) @@ -613,8 +615,9 @@ class TypeEngine(Traversible): return dialect.type_compiler.process(self) - @util.dependencies("sqlalchemy.engine.default") - def _default_dialect(self, default): + @util.preload_module("sqlalchemy.engine.default") + def _default_dialect(self): + default = util.preloaded.engine_default if self.__class__.__module__.startswith("sqlalchemy.dialects"): tokens = self.__class__.__module__.split(".")[0:3] mod = ".".join(tokens) diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py index 819d180189..1909619c59 100644 --- a/lib/sqlalchemy/util/__init__.py +++ b/lib/sqlalchemy/util/__init__.py @@ -110,7 +110,6 @@ from .langhelpers import constructor_key # noqa from .langhelpers import counter # noqa from .langhelpers import decode_slice # noqa from .langhelpers import decorator # noqa -from .langhelpers import dependencies # noqa from .langhelpers import dictlike_iteritems # noqa from .langhelpers import duck_type_collection # noqa from .langhelpers import ellipses_string # noqa @@ -137,6 +136,8 @@ from .langhelpers import NoneType # noqa from .langhelpers import only_once # noqa from .langhelpers import PluginLoader # noqa from .langhelpers import portable_instancemethod # noqa +from .langhelpers import preloaded # noqa +from .langhelpers import preload_module # noqa from .langhelpers import quoted_token_parser # noqa from .langhelpers import safe_reraise # noqa from .langhelpers import set_creation_order # noqa diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index 09aa94bf2b..bac95c7e3e 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -987,135 +987,54 @@ class MemoizedSlots(object): return self._fallback_getattr(key) -def dependency_for(modulename, add_to_all=False): - def decorate(obj): - tokens = modulename.split(".") - mod = compat.import_( - ".".join(tokens[0:-1]), globals(), locals(), [tokens[-1]] - ) - mod = getattr(mod, tokens[-1]) - setattr(mod, obj.__name__, obj) - if add_to_all and hasattr(mod, "__all__"): - mod.__all__.append(obj.__name__) - return obj - - return decorate - - -class dependencies(object): - """Apply imported dependencies as arguments to a function. - - E.g.:: - - @util.dependencies( - "sqlalchemy.sql.widget", - "sqlalchemy.engine.default" - ); - def some_func(self, widget, default, arg1, arg2, **kw): - # ... - - Rationale is so that the impact of a dependency cycle can be - associated directly with the few functions that cause the cycle, - and not pollute the module-level namespace. - +class _ModuleRegistry: + """Registry of modules to load in a package init file. + + To avoid potential thread safety issues for imports that are deferred + in a function, like https://bugs.python.org/issue38884, these modules + are added to the system module cache by importing them after the packages + has finished initialization. + + A global instance is provided under the name :attr:`.preloaded`. Use + the function :func:`.preload_module` to register modules to load and + :meth:`.import_prefix` to load all the modules that start with the + given path. + + While the modules are loaded in the global module cache, it's advisable + to access them using :attr:`.preloaded` to ensure that it was actually + registered. Each registered module is added to the instance ``__dict__`` + in the form `_`, omitting ``sqlalchemy`` from the package + name. Example: ``sqlalchemy.sql.util`` becomes ``preloaded.sql_util``. """ - def __init__(self, *deps): - self.import_deps = [] - for dep in deps: - tokens = dep.split(".") - self.import_deps.append( - dependencies._importlater(".".join(tokens[0:-1]), tokens[-1]) - ) - - def __call__(self, fn): - import_deps = self.import_deps - spec = compat.inspect_getfullargspec(fn) - - spec_zero = list(spec[0]) - hasself = spec_zero[0] in ("self", "cls") - - for i in range(len(import_deps)): - spec[0][i + (1 if hasself else 0)] = "import_deps[%r]" % i - - inner_spec = format_argspec_plus(spec, grouped=False) + def __init__(self, prefix="sqlalchemy"): + self.module_registry = set() - for impname in import_deps: - del spec_zero[1 if hasself else 0] - spec[0][:] = spec_zero + def preload_module(self, *deps): + """Adds the specified modules to the list to load. - outer_spec = format_argspec_plus(spec, grouped=False) - - code = "lambda %(args)s: fn(%(apply_kw)s)" % { - "args": outer_spec["args"], - "apply_kw": inner_spec["apply_kw"], - } - - decorated = eval(code, locals()) - decorated.__defaults__ = getattr(fn, "im_func", fn).__defaults__ - return update_wrapper(decorated, fn) - - @classmethod - def resolve_all(cls, path): - for m in list(dependencies._unresolved): - if m._full_path.startswith(path): - m._resolve() - - _unresolved = set() - _by_key = {} - - class _importlater(object): - _unresolved = set() - - _by_key = {} + This method can be used both as a normal function and as a decorator. + No change is performed to the decorated object. + """ + self.module_registry.update(deps) + return lambda fn: fn - def __new__(cls, path, addtl): - key = path + "." + addtl - if key in dependencies._by_key: - return dependencies._by_key[key] - else: - dependencies._by_key[key] = imp = object.__new__(cls) - return imp - - def __init__(self, path, addtl): - self._il_path = path - self._il_addtl = addtl - dependencies._unresolved.add(self) - - @property - def _full_path(self): - return self._il_path + "." + self._il_addtl - - @memoized_property - def module(self): - if self in dependencies._unresolved: - raise ImportError( - "importlater.resolve_all() hasn't " - "been called (this is %s %s)" - % (self._il_path, self._il_addtl) + def import_prefix(self, path): + """Resolve all the modules in the registry that start with the + specified path. + """ + for module in self.module_registry: + key = module.split("sqlalchemy.")[-1].replace(".", "_") + if module.startswith(path) and key not in self.__dict__: + tokens = module.split(".") + compat.import_( + ".".join(tokens[0:-1]), globals(), locals(), [tokens[-1]] ) + self.__dict__[key] = sys.modules[module] - return getattr(self._initial_import, self._il_addtl) - def _resolve(self): - dependencies._unresolved.discard(self) - self._initial_import = compat.import_( - self._il_path, globals(), locals(), [self._il_addtl] - ) - - def __getattr__(self, key): - if key == "module": - raise ImportError( - "Could not resolve module %s" % self._full_path - ) - try: - attr = getattr(self.module, key) - except AttributeError: - raise AttributeError( - "Module %s has no attribute '%s'" % (self._full_path, key) - ) - self.__dict__[key] = attr - return attr +preloaded = _ModuleRegistry() +preload_module = preloaded.preload_module # from paste.deploy.converters diff --git a/test/base/test_utils.py b/test/base/test_utils.py index 183e157e5e..4392df0137 100644 --- a/test/base/test_utils.py +++ b/test/base/test_utils.py @@ -3,6 +3,7 @@ import copy import datetime import inspect +import sys from sqlalchemy import exc from sqlalchemy import sql @@ -3209,3 +3210,35 @@ class TimezoneTest(fixtures.TestBase): repr(timezone(datetime.timedelta(hours=5))), "sqlalchemy.util.timezone(%r)" % (datetime.timedelta(hours=5)), ) + + +class TestModuleRegistry(fixtures.TestBase): + def test_modules_are_loaded(self): + to_restore = [] + for m in ("xml.dom", "wsgiref.simple_server"): + to_restore.append((m, sys.modules.pop(m, None))) + try: + mr = langhelpers._ModuleRegistry() + + ret = mr.preload_module( + "xml.dom", "wsgiref.simple_server", "sqlalchemy.sql.util" + ) + o = object() + is_(ret(o), o) + + is_false(hasattr(mr, "xml_dom")) + mr.import_prefix("xml") + is_true("xml.dom" in sys.modules) + is_(sys.modules["xml.dom"], mr.xml_dom) + + is_true("wsgiref.simple_server" not in sys.modules) + mr.import_prefix("wsgiref") + is_true("wsgiref.simple_server" in sys.modules) + is_(sys.modules["wsgiref.simple_server"], mr.wsgiref_simple_server) + + mr.import_prefix("sqlalchemy") + is_(sys.modules["sqlalchemy.sql.util"], mr.sql_util) + finally: + for name, mod in to_restore: + if mod is not None: + sys.modules[name] = mod diff --git a/test/ext/declarative/test_basic.py b/test/ext/declarative/test_basic.py index 935e6f4311..d81b99ccd5 100644 --- a/test/ext/declarative/test_basic.py +++ b/test/ext/declarative/test_basic.py @@ -26,10 +26,10 @@ from sqlalchemy.orm import composite from sqlalchemy.orm import configure_mappers from sqlalchemy.orm import create_session from sqlalchemy.orm import deferred +from sqlalchemy.orm import descriptor_props from sqlalchemy.orm import exc as orm_exc from sqlalchemy.orm import joinedload from sqlalchemy.orm import mapper -from sqlalchemy.orm import properties from sqlalchemy.orm import relationship from sqlalchemy.orm import Session from sqlalchemy.orm.events import MapperEvents @@ -1144,7 +1144,7 @@ class DeclarativeTest(DeclarativeTestBase): assert ASub.brap.property is A.data.property assert isinstance( - ASub.brap.original_property, properties.SynonymProperty + ASub.brap.original_property, descriptor_props.SynonymProperty ) def test_alt_name_attr_subclass_relationship_inline(self): @@ -1166,7 +1166,7 @@ class DeclarativeTest(DeclarativeTestBase): assert ASub.brap.property is A.b.property assert isinstance( - ASub.brap.original_property, properties.SynonymProperty + ASub.brap.original_property, descriptor_props.SynonymProperty ) ASub(brap=B()) @@ -1179,7 +1179,9 @@ class DeclarativeTest(DeclarativeTestBase): A.brap = A.data assert A.brap.property is A.data.property - assert isinstance(A.brap.original_property, properties.SynonymProperty) + assert isinstance( + A.brap.original_property, descriptor_props.SynonymProperty + ) def test_alt_name_attr_subclass_relationship_attrset(self): # [ticket:2900] @@ -1196,7 +1198,9 @@ class DeclarativeTest(DeclarativeTestBase): id = Column("id", Integer, primary_key=True) assert A.brap.property is A.b.property - assert isinstance(A.brap.original_property, properties.SynonymProperty) + assert isinstance( + A.brap.original_property, descriptor_props.SynonymProperty + ) A(brap=B()) def test_eager_order_by(self): diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index 29b64547ba..99e7358e5a 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -2704,7 +2704,7 @@ class ComparatorFactoryTest(_fixtures.FixtureTest, AssertsCompiledSQL): self.classes.User, ) - from sqlalchemy.orm.properties import RelationshipProperty + from sqlalchemy.orm.relationships import RelationshipProperty # NOTE: this API changed in 0.8, previously __clause_element__() # gave the parent selecatable, now it gives the diff --git a/test/profiles.txt b/test/profiles.txt index be9aa2d584..55223dc9a9 100644 --- a/test/profiles.txt +++ b/test/profiles.txt @@ -225,17 +225,17 @@ test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_entity_wo_annotations # TEST: test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set -test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 2.7_sqlite_pysqlite_dbapiunicode_cextensions 3812 -test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 3812 -test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 3.7_sqlite_pysqlite_dbapiunicode_cextensions 3933 -test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 3933 +test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 2.7_sqlite_pysqlite_dbapiunicode_cextensions 3567 +test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 3567 +test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 3.7_sqlite_pysqlite_dbapiunicode_cextensions 3688 +test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 3688 # TEST: test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove -test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove 2.7_sqlite_pysqlite_dbapiunicode_cextensions 6026 -test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 6026 -test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove 3.7_sqlite_pysqlite_dbapiunicode_cextensions 6228 -test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 6228 +test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove 2.7_sqlite_pysqlite_dbapiunicode_cextensions 5626 +test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 5626 +test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove 3.7_sqlite_pysqlite_dbapiunicode_cextensions 5828 +test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 5828 # TEST: test.aaa_profiling.test_orm.BranchedOptionTest.test_generate_path_cache_key_bound_branching diff --git a/test/test_deprecations.py b/test/test_deprecations.py index a70eea1370..19a8c54cc5 100644 --- a/test/test_deprecations.py +++ b/test/test_deprecations.py @@ -9,4 +9,4 @@ class DeprecationWarningsTest(fixtures.TestBase): with expect_deprecated_20( "The `database` package is deprecated and will be removed in v2.0 " ): - import_('sqlalchemy.databases') + import_("sqlalchemy.databases")