Issue 1:
Migrated issue, originally created by sowingsadness (@SowingSadness)

# -*- coding: utf-8 -*-
__author__ = 'SowingSadness'

from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.declarative import has_inherited_table
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.orm import relationship

Base = declarative_base()
DBSession = scoped_session(sessionmaker())
"""@type : sqlalchemy.orm.session.Session|sqlalchemy.orm.session.SessionTransaction"""

class VehicleModel(Base):
__tablename__ = "vehicle_model"
id = Column(Integer, primary_key=True)
name = Column(String(20))

class VehicleInfo(object):
vehicle_plate_region = Column(String(5))
vehicle_plate = Column(String(20))

@declared_attr
def vehicle_model_id(cls):
return Column(Integer, ForeignKey("vehicle_model.id"))

@declared_attr
def vehicle_model(cls):
return relationship(VehicleModel, foreign_keys=[cls.vehicle_model_id])

class Vehicle(VehicleInfo, Base):
__tablename__ = 'vehicle'
id = Column(Integer, primary_key=True)

def main():
engine = create_engine('postgresql+psycopg2://postgres@localhost:5432/t1')
DBSession.configure(bind=engine)
Base.metadata.bind = engine

Base.metadata.drop_all()
Base.metadata.create_all(checkfirst=True)

vm = VehicleModel()
vehicle = Vehicle(vehicle_model=vm)
DBSession.add(vm)
DBSession.add(vehicle)
DBSession.commit()

if __name__ == '__main__':
main()

C:\Users\Kir\Documents\Work\pyramid_p27\Scripts\pythonw.exe C:/Users/Kir/Documents/Work/sqlalchemy-test/main.py
Traceback (most recent call last):
File "C:/Users/Kir/Documents/Work/sqlalchemy-test/main.py", line 58, in <module>
main()
File "C:/Users/Kir/Documents/Work/sqlalchemy-test/main.py", line 50, in main
vm = VehicleModel()
File "<string>", line 2, in __init__
File "C:\Users\Kir\Documents\Work\pyramid_p27\lib\site-packages\sqlalchemy\orm\instrumentation.py", line 324, in _new_state_if_none
state = self._state_constructor(instance, self)
File "C:\Users\Kir\Documents\Work\pyramid_p27\lib\site-packages\sqlalchemy\util\langhelpers.py", line 725, in __get__
obj.__dict__[self.__name__] = result = self.fget(obj)
File "C:\Users\Kir\Documents\Work\pyramid_p27\lib\site-packages\sqlalchemy\orm\instrumentation.py", line 158, in _state_constructor
self.dispatch.first_init(self, self.class_)
File "C:\Users\Kir\Documents\Work\pyramid_p27\lib\site-packages\sqlalchemy\event\attr.py", line 260, in __call__
fn(*args, **kw)
File "C:\Users\Kir\Documents\Work\pyramid_p27\lib\site-packages\sqlalchemy\orm\mapper.py", line 2687, in _event_on_first_init
configure_mappers()
File "C:\Users\Kir\Documents\Work\pyramid_p27\lib\site-packages\sqlalchemy\orm\mapper.py", line 2583, in configure_mappers
mapper._post_configure_properties()
File "C:\Users\Kir\Documents\Work\pyramid_p27\lib\site-packages\sqlalchemy\orm\mapper.py", line 1688, in _post_configure_properties
prop.init()
File "C:\Users\Kir\Documents\Work\pyramid_p27\lib\site-packages\sqlalchemy\orm\interfaces.py", line 144, in init
self.do_init()
File "C:\Users\Kir\Documents\Work\pyramid_p27\lib\site-packages\sqlalchemy\orm\relationships.py", line 1550, in do_init
self._setup_join_conditions()
File "C:\Users\Kir\Documents\Work\pyramid_p27\lib\site-packages\sqlalchemy\orm\relationships.py", line 1624, in _setup_join_conditions
can_be_synced_fn=self._columns_are_mapped
File "C:\Users\Kir\Documents\Work\pyramid_p27\lib\site-packages\sqlalchemy\orm\relationships.py", line 1892, in __init__
self._determine_joins()
File "C:\Users\Kir\Documents\Work\pyramid_p27\lib\site-packages\sqlalchemy\orm\relationships.py", line 1996, in _determine_joins
"specify a 'primaryjoin' expression." % self.prop)
sqlalchemy.exc.NoForeignKeysError: Could not determine join condition between parent/child tables on relationship Vehicle.vehicle_model - there are no foreign keys linking these tables. Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression.

Process finished with exit code 1

I try to use code in https://bitbucket.org/zzzeek/sqlalchemy/issue/2471/add-example-of-declared_attr-columns but result is same.

Issue 2:
Migrated issue, originally created by Michael Bayer (@zzzeek)

poc should allow us to consolidate #2670, #2952, #3149, #3050.

the new descriptors include the ability to cache the result per class, or to do "cascade", guarantees that the callable fn is called only once per target class, as well as to name attributes that are set up after the mapping is complete, so that relationship and column_property declared_attrs have the whole mapping to work with when they are called. and the whole thing doesn't modify any existing functionality, only adds new things we can take time to stabilize. win win win win.

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base, declared_attr, has_inherited_table

Base = declarative_base()

class VehicleModel(Base):
__tablename__ = "vehicle_model"
id = Column(Integer, primary_key=True)
name = Column(String(20))

class VehicleInfo(object):
vehicle_plate_region = Column(String(5))
vehicle_plate = Column(String(20))

@declared_attr.column
def vehicle_model_id(cls):
return Column(Integer, ForeignKey("vehicle_model.id"))

@declared_attr.property
def vehicle_model(cls):
# 1. called after the class is fully mapped
# 2. called only once for each class

assert cls.__table__ is not None and \
cls.__table__.c.vehicle_model_id.shares_lineage(
cls.vehicle_model_id.__clause_element__()
)

return relationship(VehicleModel, foreign_keys=[cls.vehicle_model_id])

@declared_attr.column.cascading
def id(cls):
if has_inherited_table(cls):
return Column(Integer, ForeignKey("vehicle.id"), primary_key=True)
else:
return Column(Integer, primary_key=True)

class Vehicle(VehicleInfo, Base):
__tablename__ = 'vehicle'

class SubVehicle(Vehicle):
__tablename__ = 'subveh'

@declared_attr.property
def some_other_thing(cls):
# called way at the end
assert cls.id.__clause_element__().references(Vehicle.__table__.c.id)
return column_property(cls.id)

configure_mappers()

poc patch, however still needs integration for #2670:

diff --git a/lib/sqlalchemy/ext/declarative/api.py b/lib/sqlalchemy/ext/declarative/api.py
index daf8bff..251b06f 100644
--- a/lib/sqlalchemy/ext/declarative/api.py
+++ b/lib/sqlalchemy/ext/declarative/api.py
@@ -13,7 +13,7 @@ from ...orm import synonym as _orm_synonym, mapper,\
interfaces, properties
from ...orm.util import polymorphic_union
from ...orm.base import _mapper_or_none
-from ...util import OrderedDict
+from ...util import OrderedDict, classproperty
from ... import exc
import weakref

@@ -164,6 +164,45 @@ class declared_attr(interfaces._MappedAttribute, property):
def __get__(desc, self, cls):
return desc.fget(cls)

+ @classproperty
+ def column(cls):
+ return _declared_column
+
+ @classproperty
+ def property(cls):
+ return _declared_property
+
+ defer_defer_defer = False
+
+
+class _memoized_declared_attr(declared_attr):
+ def __init__(self, fget, cascading=False):
+ super(_memoized_declared_attr, self).__init__(fget)
+ self.reg = weakref.WeakKeyDictionary()
+ self._cascading = cascading
+
+ def __get__(desc, self, cls):
+ if desc.defer_defer_defer:
+ return desc
+ elif cls in desc.reg:
+ return desc.reg[cls]
+ else:
+ desc.reg[cls] = obj = desc.fget(cls)
+ return obj
+
+ @classproperty
+ def cascading(cls):
+ return lambda decorated: cls(decorated, cascading=True)
+
+
+class _declared_column(_memoized_declared_attr):
+ pass
+
+
+class _declared_property(_memoized_declared_attr):
+ defer_defer_defer = True
+
+

def declarative_base(bind=None, metadata=None, mapper=None, cls=object,
name='Base', constructor=_declarative_constructor,
diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py
index 94baeeb..cee2263 100644
--- a/lib/sqlalchemy/ext/declarative/base.py
+++ b/lib/sqlalchemy/ext/declarative/base.py
@@ -33,7 +33,7 @@ def _declared_mapping_info(cls):

def _as_declarative(cls, classname, dict_):
- from .api import declared_attr
+ from .api import declared_attr, _memoized_declared_attr

# dict_ will be a dictproxy, which we can't write to, and we need to!
dict_ = dict(dict_)
@@ -132,6 +132,14 @@ def _as_declarative(cls, classname, dict_):
"column_property(), relationship(), etc.) must "
"be declared as @declared_attr callables "
"on declarative mixin classes.")
+ elif isinstance(obj, _memoized_declared_attr): # and \
+ if obj._cascading:
+ dict_[name] = ret = obj.__get__(obj, cls)
+ else:
+ dict_[name] = ret = getattr(cls, name)
+ if isinstance(ret, (Column, MapperProperty)) and \
+ ret.doc is None:
+ ret.doc = obj.__doc__
elif isinstance(obj, declarative_props):
dict_[name] = ret = \
column_copies[obj] = getattr(cls, name)
@@ -148,6 +156,7 @@ def _as_declarative(cls, classname, dict_):

clsregistry.add_class(classname, cls)
our_stuff = util.OrderedDict()
+ add_later = util.OrderedDict()

for k in list(dict_):

@@ -157,7 +166,10 @@ def _as_declarative(cls, classname, dict_):

value = dict_[k]
if isinstance(value, declarative_props):
- value = getattr(cls, k)
+ if value.defer_defer_defer:
+ add_later[k] = value
+ else:
+ value = getattr(cls, k)

elif isinstance(value, QueryableAttribute) and \
value.class_ is not cls and \
@@ -324,7 +336,8 @@ def _as_declarative(cls, classname, dict_):
declared_columns,
column_copies,
our_stuff,
- mapper_args_fn)
+ mapper_args_fn,
+ add_later)
if not defer_map:
mt.map()

@@ -339,7 +352,8 @@ class _MapperConfig(object):
inherits,
declared_columns,
column_copies,
- properties, mapper_args_fn):
+ properties, mapper_args_fn,
+ add_later):
self.mapper_cls = mapper_cls
self.cls = cls
self.local_table = table
@@ -348,6 +362,7 @@ class _MapperConfig(object):
self.mapper_args_fn = mapper_args_fn
self.declared_columns = declared_columns
self.column_copies = column_copies
+ self.add_later = add_later

def _prepare_mapper_arguments(self):
properties = self.properties
@@ -410,6 +425,8 @@ class _MapperConfig(object):
self.local_table,
**mapper_args
)
+ for k, v in self.add_later.items():
+ setattr(self.cls, k, v.fget(self.cls))

class _DeferredMapperConfig(_MapperConfig):
diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py
index f3af46c..ea8c32f 100644
--- a/lib/sqlalchemy/sql/schema.py
+++ b/lib/sqlalchemy/sql/schema.py
@@ -1161,8 +1161,10 @@ class Column(SchemaItem, ColumnClause):
existing = getattr(self, 'table', None)
if existing is not None and existing is not table:
raise exc.ArgumentError(
- "Column object already assigned to Table '%s'" %
- existing.description)
+ "Column object '%s' already assigned to Table '%s'" % (
+ self.key,
+ existing.description
+ ))

if self.key in table._columns:
col = table._columns.get(self.key)