diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index d4c9672b..8b18bb0c 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -8,9 +8,7 @@ from datetime import datetime, timedelta import alembic.command import alembic.config -import sqlalchemy from alembic.script import ScriptDirectory -from packaging.version import parse as parse_version from sqlalchemy import ( Boolean, Column, @@ -31,18 +29,12 @@ from sqlalchemy import ( from sqlalchemy.orm import ( Session, backref, + declarative_base, interfaces, object_session, relationship, sessionmaker, ) - -try: - from sqlalchemy.orm import declarative_base -except ImportError: - # sqlalchemy < 1.4 - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.pool import StaticPool from sqlalchemy.types import LargeBinary, Text, TypeDecorator from tornado.log import app_log @@ -912,27 +904,19 @@ def register_ping_connection(engine): @event.listens_for(engine, "engine_connect") def ping_connection(connection, branch=None): - if branch: - # "branch" refers to a sub-connection of a connection, - # we don't want to bother pinging on these. - return + # TODO: remove unused branch arg when we require sqlalchemy 2.0 # turn off "close with result". This flag is only used with # "connectionless" execution, otherwise will be False in any case save_should_close_with_result = connection.should_close_with_result connection.should_close_with_result = False - if parse_version(sqlalchemy.__version__) < parse_version("1.4"): - one = [1] - else: - one = 1 - try: # run a SELECT 1. use a core select() so that # the SELECT of a scalar value without a table is # appropriately formatted for the backend with connection.begin() as transaction: - connection.scalar(select(one)) + connection.scalar(select(1)) except exc.DBAPIError as err: # catch SQLAlchemy's DBAPIError, which is a wrapper # for the DBAPI's exception. It includes a .connection_invalidated @@ -948,7 +932,7 @@ def register_ping_connection(engine): # here also causes the whole connection pool to be invalidated # so that all stale connections are discarded. with connection.begin() as transaction: - connection.scalar(select(one)) + connection.scalar(select(1)) else: raise finally: @@ -972,11 +956,8 @@ def check_db_revision(engine): from .dbutil import _temp_alembic_ini - if hasattr(engine.url, "render_as_string"): - # sqlalchemy >= 1.4 - engine_url = engine.url.render_as_string(hide_password=False) - else: - engine_url = str(engine.url) + # alembic needs the password if it's in the URL + engine_url = engine.url.render_as_string(hide_password=False) with _temp_alembic_ini(engine_url) as ini: cfg = alembic.config.Config(ini) @@ -1067,6 +1048,8 @@ def new_session_factory( elif url.startswith('mysql'): kwargs.setdefault('pool_recycle', 60) + kwargs.setdefault("future", True) + if url.endswith(':memory:'): # If we're using an in-memory database, ensure that only one connection # is ever created. diff --git a/jupyterhub/tests/test_db.py b/jupyterhub/tests/test_db.py index 9c89abb6..ee9d0bbd 100644 --- a/jupyterhub/tests/test_db.py +++ b/jupyterhub/tests/test_db.py @@ -5,6 +5,7 @@ from glob import glob from subprocess import check_call import pytest +from packaging.version import parse as V from pytest import raises from traitlets.config import Config @@ -25,8 +26,14 @@ def generate_old_db(env_dir, hub_version, db_url): env_pip = os.path.join(env_dir, 'bin', 'pip') env_py = os.path.join(env_dir, 'bin', 'python') check_call([sys.executable, '-m', 'virtualenv', env_dir]) + pkgs = ['jupyterhub==' + hub_version] + # older jupyterhub needs older sqlachemy version - pkgs = ['jupyterhub==' + hub_version, 'sqlalchemy<1.4'] + if V(hub_version) < V("2"): + pkgs.append('sqlalchemy<1.4') + elif V(hub_version) < V("3.1.1"): + pkgs.append('sqlalchemy<2') + if 'mysql' in db_url: pkgs.append('mysql-connector-python') elif 'postgres' in db_url: diff --git a/jupyterhub/tests/utils.py b/jupyterhub/tests/utils.py index 6bae468d..54222658 100644 --- a/jupyterhub/tests/utils.py +++ b/jupyterhub/tests/utils.py @@ -14,20 +14,6 @@ from jupyterhub.objects import Server from jupyterhub.roles import assign_default_roles, update_roles from jupyterhub.utils import url_path_join as ujoin -try: - from sqlalchemy.exc import RemovedIn20Warning -except ImportError: - - class RemovedIn20Warning(DeprecationWarning): - """ - I only exist so I can be used in warnings filters in pytest.ini - - I will never be displayed. - - sqlalchemy 1.4 introduces RemovedIn20Warning, - but we still test against older sqlalchemy. - """ - class _AsyncRequests: """Wrapper around requests to return a Future from request methods diff --git a/pytest.ini b/pytest.ini index 1a57882e..b594142e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -20,5 +20,5 @@ markers = selenium: web tests that run with selenium filterwarnings = - error:.*:jupyterhub.tests.utils.RemovedIn20Warning - ignore:.*event listener has changed as of version 2.0.*:sqlalchemy.exc.SADeprecationWarning + ignore:.*The new signature is "def engine_connect\(conn\)"*:sqlalchemy.exc.SADeprecationWarning + ignore:.*The new signature is "def engine_connect\(conn\)"*:sqlalchemy.exc.SAWarning diff --git a/requirements.txt b/requirements.txt index 21de4a74..384e6a9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,6 @@ prometheus_client>=0.4.0 psutil>=5.6.5; sys_platform == 'win32' python-dateutil requests -SQLAlchemy>=1.1 +SQLAlchemy>=1.4 tornado>=5.1 traitlets>=4.3.2