-
Notifications
You must be signed in to change notification settings - Fork 80
Support SQLAlchemy 2.0 #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
220d092
66e3941
7e0854a
c94c862
db58232
407f7c3
dc7786e
b45a30c
7fcb4c1
a951949
389b17c
debf911
a0dea62
d7d2246
526375f
39f54fc
bcd4848
73bed5e
9fd5f99
1865f0d
bef9e82
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,3 +10,8 @@ __pycache__/ | |
.coverage.* | ||
.cache | ||
.tox | ||
|
||
venv | ||
.venv | ||
.idea | ||
build |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
3.7.17 | ||
3.8.18 | ||
3.9.18 | ||
3.10.13 | ||
3.11.7 | ||
3.12.1 | ||
Comment on lines
+1
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I locally use pyenv and thought this would be useful to specify. Happy to remove if this muddies the waters for tooling. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,9 +31,10 @@ | |
'restructuredtext-lint', | ||
'Pygments', | ||
'coverage-conditional-plugin', | ||
'tox~=3.28' | ||
], | ||
'mysql': ['mysql-connector-python-rf==2.2.2'], | ||
'postgresql': ['psycopg2==2.8.4'], | ||
'mysql': ['mysql-connector-python-rf>=2.2.2'], | ||
'postgresql': ['psycopg2>=2.8.4'], | ||
Comment on lines
-35
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't exactly recall the Python version that was building wheels. I didn't think the versions were all that important for validating proper execution. I can look into this deeper if necessary. |
||
}, | ||
zip_safe=True, | ||
license='Apache License, Version 2.0', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,22 @@ | ||
import operator | ||
|
||
from sqlalchemy import __version__ as sqlalchemy_version | ||
from sqlalchemy.exc import InvalidRequestError | ||
from sqlalchemy.orm import mapperlib | ||
from sqlalchemy.inspection import inspect | ||
from sqlalchemy.util import symbol | ||
import types | ||
|
||
from .exceptions import BadQuery, FieldNotFound, BadSpec | ||
|
||
|
||
def sqlalchemy_version_lt(version): | ||
def sqlalchemy_version_cmp(op, version): | ||
"""compares sqla version < version""" | ||
|
||
return tuple(sqlalchemy_version.split('.')) < tuple(version.split('.')) | ||
ops = {'<': operator.lt, '>=': operator.ge} | ||
return ops[op]( | ||
tuple(sqlalchemy_version.split('.')), | ||
tuple(version.split('.')) | ||
) | ||
|
||
|
||
class Field(object): | ||
|
@@ -51,12 +56,15 @@ def _get_valid_field_names(self): | |
|
||
|
||
def _is_hybrid_property(orm_descriptor): | ||
return orm_descriptor.extension_type == symbol('HYBRID_PROPERTY') | ||
# SQLAlchemy 2 treats extension_type as an enum, not a symbol(). Enum is at sqlalchemy.ext.hybrid.HybridExtensionType | ||
|
||
return str(orm_descriptor.extension_type) in ("symbol('HYBRID_PROPERTY')", 'HybridExtensionType.HYBRID_PROPERTY') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a little bit hacky but I was trying to optimize for performance. Other things I thought of:
Happy to change for a different opinion on the right way to check this. |
||
|
||
|
||
def _is_hybrid_method(orm_descriptor): | ||
return orm_descriptor.extension_type == symbol('HYBRID_METHOD') | ||
# SQLAlchemy 2 treats extension_type as an enum, not a symbol(). Enum is at sqlalchemy.ext.hybrid.HybridExtensionType | ||
|
||
return str(orm_descriptor.extension_type) in ("symbol('HYBRID_METHOD')", 'HybridExtensionType.HYBRID_METHOD') | ||
|
||
def get_model_from_table(table): # pragma: no_cover_sqlalchemy_lt_1_4 | ||
"""Resolve model class from table object""" | ||
|
@@ -68,7 +76,7 @@ def get_model_from_table(table): # pragma: no_cover_sqlalchemy_lt_1_4 | |
return None | ||
|
||
|
||
def get_query_models(query): | ||
def get_query_models(query): # pragma: nocover | ||
"""Get models from query. | ||
|
||
:param query: | ||
|
@@ -80,39 +88,39 @@ def get_query_models(query): | |
models = [col_desc['entity'] for col_desc in query.column_descriptions] | ||
|
||
# account joined entities | ||
if sqlalchemy_version_lt('1.4'): # pragma: no_cover_sqlalchemy_gte_1_4 | ||
if sqlalchemy_version_cmp('<', '1.4'): | ||
models.extend(mapper.class_ for mapper in query._join_entities) | ||
else: # pragma: no_cover_sqlalchemy_lt_1_4 | ||
else: | ||
try: | ||
models.extend( | ||
mapper.class_ | ||
for mapper | ||
in query._compile_state()._join_entities | ||
) | ||
except InvalidRequestError: | ||
except (InvalidRequestError, AttributeError): | ||
# query might not contain columns yet, hence cannot be compiled | ||
# try to infer the models from various internals | ||
for table_tuple in query._setup_joins + query._legacy_setup_joins: | ||
model_class = get_model_from_table(table_tuple[0]) | ||
if model_class: | ||
models.append(model_class) | ||
# or query might be a sqla2.0 select statement | ||
pass | ||
# also try to infer the models from various internals | ||
all_joins = query._setup_joins | ||
if hasattr(query, "_legacy_setup_joins"): | ||
all_joins += query._legacy_setup_joins | ||
|
||
for table_tuple in all_joins: | ||
models.append(get_model_from_table(table_tuple[0])) | ||
|
||
# account also query.select_from entities | ||
model_class = None | ||
if sqlalchemy_version_lt('1.4'): # pragma: no_cover_sqlalchemy_gte_1_4 | ||
if sqlalchemy_version_cmp('<', '1.1'): # sqla 1.0 | ||
if query._select_from_entity: | ||
model_class = ( | ||
query._select_from_entity | ||
if sqlalchemy_version_lt('1.1') | ||
else query._select_from_entity.class_ | ||
) | ||
else: # pragma: no_cover_sqlalchemy_lt_1_4 | ||
models.append(query._select_from_entity) | ||
elif sqlalchemy_version_cmp('<', '1.4'): # sqla 1.1-1.3 | ||
if query._select_from_entity: | ||
models.append(query._select_from_entity.class_) | ||
else: # sqla 1.4 | ||
if query._from_obj: | ||
model_class = get_model_from_table(query._from_obj[0]) | ||
if model_class and (model_class not in models): | ||
models.append(model_class) | ||
models.append(get_model_from_table(query._from_obj[0])) | ||
|
||
return {model.__name__: model for model in models} | ||
return {model.__name__: model for model in models if model is not None} | ||
|
||
|
||
def get_model_from_spec(spec, query, default_model=None): | ||
|
@@ -191,23 +199,24 @@ def auto_join(query, *model_names): | |
last_model = list(query_models)[-1] | ||
model_registry = ( | ||
last_model._decl_class_registry | ||
if sqlalchemy_version_lt('1.4') | ||
if sqlalchemy_version_cmp('<', '1.4') | ||
else last_model.registry._class_registry | ||
) | ||
|
||
for name in model_names: | ||
model = get_model_class_by_name(model_registry, name) | ||
if model and (model not in get_query_models(query).values()): | ||
try: | ||
if sqlalchemy_version_lt('1.4'): # pragma: no_cover_sqlalchemy_gte_1_4 | ||
query = query.join(model) | ||
else: # pragma: no_cover_sqlalchemy_lt_1_4 | ||
tmp = query.join(model) | ||
if ( | ||
sqlalchemy_version_cmp('>=', '1.4') | ||
and hasattr(tmp, '_compile_state') | ||
): # pragma: nocover | ||
# https://docs.sqlalchemy.org/en/14/changelog/migration_14.html | ||
# Many Core and ORM statement objects now perform much of | ||
# their construction and validation in the compile phase | ||
tmp = query.join(model) | ||
tmp._compile_state() | ||
query = tmp | ||
query = tmp | ||
except InvalidRequestError: | ||
pass # can't be autojoined | ||
return query |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -131,7 +131,7 @@ def connection(db_uri, db_engine_options, is_postgresql): | |
|
||
yield connection | ||
|
||
Base.metadata.drop_all() | ||
Base.metadata.drop_all(engine) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SA 2.x compatibility |
||
destroy_database(db_uri) | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,16 @@ | ||
[tox] | ||
envlist = {py37,py38,py39,py310}-sqlalchemy{1.0,1.1,1.2,1.3,1.4,latest} | ||
envlist = {py37,py38,py39}-sqlalchemy{1.0,1.1,1.2,1.3,1.4,2,latest},{py310,py311,py312}-sqlalchemy{1.2,1.3,1.4,2,latest} | ||
skipsdist = True | ||
|
||
[gh-actions] | ||
python = | ||
3.7: py37 | ||
3.8: py38 | ||
3.9: py39 | ||
3.10: py310 | ||
3.11: py311 | ||
3.12: py312 | ||
|
||
[testenv] | ||
whitelist_externals = make | ||
usedevelop = true | ||
|
@@ -10,11 +19,14 @@ extras = | |
mysql | ||
postgresql | ||
deps = | ||
{py37,py38,py39,py310}: sqlalchemy-utils~=0.37.8 | ||
!sqlalchemy2,!sqlalchemylatest: sqlalchemy-utils~=0.37.8 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really wanted |
||
py311,py312: sqlalchemy-utils>=0.39 | ||
sqlalchemy{2,latest}: sqlalchemy-utils>=0.38.3 | ||
sqlalchemy1.0: sqlalchemy>=1.0,<1.1 | ||
sqlalchemy1.1: sqlalchemy>=1.1,<1.2 | ||
sqlalchemy1.2: sqlalchemy>=1.2,<1.3 | ||
sqlalchemy1.3: sqlalchemy>=1.3,<1.4 | ||
sqlalchemy1.4: sqlalchemy>=1.4,<1.5 | ||
sqlalchemy2: sqlalchemy>=2.0,<2.1 | ||
commands = | ||
make coverage ARGS='-x -vv' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No longer necessary since this is now fully encoded in tox.ini.