From d71af220f426710c431e334b10ac1e6b9cb79b27 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Wed, 20 Dec 2023 04:56:24 +0100 Subject: [PATCH] Split dialect from driver, part 3: Swap in SQLAlchemy dialect and tests Move SQLAlchemy dialect implementation to the top level. --- .github/workflows/tests.yml | 9 +- README.md | 2 +- backlog.md | 12 ++ pyproject.toml | 2 + src/crate/client/sqlalchemy/__init__.py | 50 ----- .../client/sqlalchemy/compat/__init__.py | 0 src/crate/client/test_util.py | 69 ------- src/crate/client/tests.py | 185 +----------------- src/crate/testing/__init__.py | 0 src/crate/testing/tests.py | 34 ---- src/sqlalchemy_cratedb/__init__.py | 57 +++++- .../compat}/__init__.py | 0 .../compat/api13.py | 0 .../compat/core10.py | 2 +- .../compat/core14.py | 2 +- .../compat/core20.py | 2 +- .../compiler.py | 0 .../dialect.py | 0 .../predicates.py} | 0 .../sa_version.py | 0 .../support.py | 0 .../types.py | 0 .../sqlalchemy/tests => tests}/__init__.py | 6 +- .../sqlalchemy/tests => tests}/array_test.py | 0 .../sqlalchemy/tests => tests}/bulk_test.py | 6 +- .../tests => tests}/compiler_test.py | 10 +- .../tests => tests}/connection_test.py | 0 .../tests => tests}/create_table_test.py | 5 +- .../tests => tests}/datetime_test.py | 0 .../tests => tests}/dialect_test.py | 6 +- .../sqlalchemy/tests => tests}/dict_test.py | 2 +- .../tests => tests}/function_test.py | 0 .../insert_from_select_test.py | 0 .../sqlalchemy/tests => tests}/match_test.py | 4 +- .../tests => tests}/query_caching.py | 4 +- src/crate/__init__.py => tests/settings.py | 8 + tests/test_dummy.py | 5 - .../sqlalchemy/tests => tests}/update_test.py | 2 +- tests/util.py | 47 +++++ .../tests => tests}/warnings_test.py | 12 +- 40 files changed, 169 insertions(+), 374 deletions(-) create mode 100644 backlog.md delete mode 100644 src/crate/client/sqlalchemy/__init__.py delete mode 100644 src/crate/client/sqlalchemy/compat/__init__.py delete mode 100644 src/crate/client/test_util.py delete mode 100644 src/crate/testing/__init__.py delete mode 100644 src/crate/testing/tests.py rename src/{crate/client => sqlalchemy_cratedb/compat}/__init__.py (100%) rename src/{crate/client/sqlalchemy => sqlalchemy_cratedb}/compat/api13.py (100%) rename src/{crate/client/sqlalchemy => sqlalchemy_cratedb}/compat/core10.py (99%) rename src/{crate/client/sqlalchemy => sqlalchemy_cratedb}/compat/core14.py (99%) rename src/{crate/client/sqlalchemy => sqlalchemy_cratedb}/compat/core20.py (99%) rename src/{crate/client/sqlalchemy => sqlalchemy_cratedb}/compiler.py (100%) rename src/{crate/client/sqlalchemy => sqlalchemy_cratedb}/dialect.py (100%) rename src/{crate/client/sqlalchemy/predicates/__init__.py => sqlalchemy_cratedb/predicates.py} (100%) rename src/{crate/client/sqlalchemy => sqlalchemy_cratedb}/sa_version.py (100%) rename src/{crate/client/sqlalchemy => sqlalchemy_cratedb}/support.py (100%) rename src/{crate/client/sqlalchemy => sqlalchemy_cratedb}/types.py (100%) rename {src/crate/client/sqlalchemy/tests => tests}/__init__.py (91%) rename {src/crate/client/sqlalchemy/tests => tests}/array_test.py (100%) rename {src/crate/client/sqlalchemy/tests => tests}/bulk_test.py (98%) rename {src/crate/client/sqlalchemy/tests => tests}/compiler_test.py (98%) rename {src/crate/client/sqlalchemy/tests => tests}/connection_test.py (100%) rename {src/crate/client/sqlalchemy/tests => tests}/create_table_test.py (98%) rename {src/crate/client/sqlalchemy/tests => tests}/datetime_test.py (100%) rename {src/crate/client/sqlalchemy/tests => tests}/dialect_test.py (97%) rename {src/crate/client/sqlalchemy/tests => tests}/dict_test.py (99%) rename {src/crate/client/sqlalchemy/tests => tests}/function_test.py (100%) rename {src/crate/client/sqlalchemy/tests => tests}/insert_from_select_test.py (100%) rename {src/crate/client/sqlalchemy/tests => tests}/match_test.py (97%) rename {src/crate/client/sqlalchemy/tests => tests}/query_caching.py (97%) rename src/crate/__init__.py => tests/settings.py (82%) delete mode 100644 tests/test_dummy.py rename {src/crate/client/sqlalchemy/tests => tests}/update_test.py (98%) create mode 100644 tests/util.py rename {src/crate/client/sqlalchemy/tests => tests}/warnings_test.py (86%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b63db53e..0ad03d81 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,8 +24,8 @@ jobs: sqla-version: ['<1.4', '<1.5', '<2.1'] pip-allow-prerelease: ['false'] - # To save resources, only use the most recent Python versions on macOS. exclude: + # To save resources, only use the most recent Python versions on macOS. - os: 'macos-latest' python-version: '3.7' - os: 'macos-latest' @@ -34,6 +34,13 @@ jobs: python-version: '3.9' - os: 'macos-latest' python-version: '3.10' + # SQLAlchemy 1.3 is not supported on Python 3.12 and higher. + - os: 'ubuntu-latest' + python-version: '3.12' + sqla-version: '<1.4' + - os: 'macos-latest' + python-version: '3.12' + sqla-version: '<1.4' # Another CI test matrix slot to test against prerelease versions of Python packages. include: diff --git a/README.md b/README.md index b5526411..bacb8d6d 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ library [crate-python]. The package is available from [PyPI] at [sqlalchemy-cratedb]. To install the most recent version, run: ```shell -pip install --upgrade cratedb-sqlalchemy +pip install --upgrade sqlalchemy-cratedb ``` ## Documentation and help diff --git a/backlog.md b/backlog.md new file mode 100644 index 00000000..2e3f75de --- /dev/null +++ b/backlog.md @@ -0,0 +1,12 @@ +# Backlog + +## Iteration +1 +From dialect split-off. +- Re-enable parameterized test cases. + ``` + tests.addTest(ParametrizedTestCase.parametrize(SqlAlchemyCompilerTest, param={"server_version_info": None})) + tests.addTest(ParametrizedTestCase.parametrize(SqlAlchemyCompilerTest, param={"server_version_info": (4, 0, 12)})) + tests.addTest(ParametrizedTestCase.parametrize(SqlAlchemyCompilerTest, param={"server_version_info": (4, 1, 10)})) + ``` +- Re-enable doctests. + diff --git a/pyproject.toml b/pyproject.toml index 7d462074..81d166da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,6 +119,8 @@ changelog = "https://github.com/crate-workbench/sqlalchemy-cratedb/blob/main/CHA documentation = "https://github.com/crate-workbench/sqlalchemy-cratedb" homepage = "https://github.com/crate-workbench/sqlalchemy-cratedb" repository = "https://github.com/crate-workbench/sqlalchemy-cratedb" +[project.entry-points."sqlalchemy.dialects"] +crate = "sqlalchemy_cratedb:CrateDialect" [tool.black] line-length = 80 diff --git a/src/crate/client/sqlalchemy/__init__.py b/src/crate/client/sqlalchemy/__init__.py deleted file mode 100644 index 2a7a1da7..00000000 --- a/src/crate/client/sqlalchemy/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8; -*- -# -# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor -# license agreements. See the NOTICE file distributed with this work for -# additional information regarding copyright ownership. Crate licenses -# this file to you under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. You may -# obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# However, if you have executed another commercial license agreement -# with Crate these terms will supersede the license and you may use the -# software solely pursuant to the terms of the relevant commercial agreement. - -from .compat.api13 import monkeypatch_add_exec_driver_sql -from .dialect import CrateDialect -from .sa_version import SA_1_4, SA_VERSION - - -if SA_VERSION < SA_1_4: - import textwrap - import warnings - - # SQLAlchemy 1.3 is effectively EOL. - SA13_DEPRECATION_WARNING = textwrap.dedent(""" - WARNING: SQLAlchemy 1.3 is effectively EOL. - - SQLAlchemy 1.3 is EOL since 2023-01-27. - Future versions of the CrateDB SQLAlchemy dialect will drop support for SQLAlchemy 1.3. - It is recommended that you transition to using SQLAlchemy 1.4 or 2.0: - - - https://docs.sqlalchemy.org/en/14/changelog/migration_14.html - - https://docs.sqlalchemy.org/en/20/changelog/migration_20.html - """.lstrip("\n")) - warnings.warn(message=SA13_DEPRECATION_WARNING, category=DeprecationWarning) - - # SQLAlchemy 1.3 does not have the `exec_driver_sql` method, so add it. - monkeypatch_add_exec_driver_sql() - - -__all__ = [ - CrateDialect, -] diff --git a/src/crate/client/sqlalchemy/compat/__init__.py b/src/crate/client/sqlalchemy/compat/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/crate/client/test_util.py b/src/crate/client/test_util.py deleted file mode 100644 index 823a44e3..00000000 --- a/src/crate/client/test_util.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8; -*- -# -# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor -# license agreements. See the NOTICE file distributed with this work for -# additional information regarding copyright ownership. Crate licenses -# this file to you under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. You may -# obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# However, if you have executed another commercial license agreement -# with Crate these terms will supersede the license and you may use the -# software solely pursuant to the terms of the relevant commercial agreement. -import unittest - - -class ClientMocked(object): - - active_servers = ["http://localhost:4200"] - - def __init__(self): - self.response = {} - self._server_infos = ("http://localhost:4200", "my server", "2.0.0") - - def sql(self, stmt=None, parameters=None, bulk_parameters=None): - return self.response - - def server_infos(self, server): - return self._server_infos - - def set_next_response(self, response): - self.response = response - - def set_next_server_infos(self, server, server_name, version): - self._server_infos = (server, server_name, version) - - def close(self): - pass - - -class ParametrizedTestCase(unittest.TestCase): - """ - TestCase classes that want to be parametrized should - inherit from this class. - - https://eli.thegreenplace.net/2011/08/02/python-unit-testing-parametrized-test-cases - """ - def __init__(self, methodName="runTest", param=None): - super(ParametrizedTestCase, self).__init__(methodName) - self.param = param - - @staticmethod - def parametrize(testcase_klass, param=None): - """ Create a suite containing all tests taken from the given - subclass, passing them the parameter 'param'. - """ - testloader = unittest.TestLoader() - testnames = testloader.getTestCaseNames(testcase_klass) - suite = unittest.TestSuite() - for name in testnames: - suite.addTest(testcase_klass(name, param=param)) - return suite diff --git a/src/crate/client/tests.py b/src/crate/client/tests.py index e242f54f..a318d6de 100644 --- a/src/crate/client/tests.py +++ b/src/crate/client/tests.py @@ -21,47 +21,19 @@ from __future__ import absolute_import -import json -import os -import socket import sys import unittest import doctest from pprint import pprint -from http.server import HTTPServer, BaseHTTPRequestHandler -import ssl -import time -import threading import logging -import stopit - -from crate.testing.layer import CrateLayer -from crate.testing.settings import \ - crate_host, crate_path, crate_port, \ - crate_transport_port, docs_path, localhost +from crate.testing.settings import crate_host, docs_path from crate.client import connect -from .sqlalchemy import SA_VERSION, SA_1_4 - -from .test_cursor import CursorTest -from .test_connection import ConnectionTest -from .test_http import ( - HttpClientTest, - ThreadSafeHttpClientTest, - KeepAliveClientTest, - ParamsTest, - RetryOnTimeoutServerTest, - RequestsCaBundleTest, - TestUsernameSentAsHeader, - TestCrateJsonEncoder, - TestDefaultSchemaHeader, -) -from .sqlalchemy.tests import test_suite_unit as sqlalchemy_test_suite_unit -from .sqlalchemy.tests import test_suite_integration as sqlalchemy_test_suite_integration +from sqlalchemy_cratedb import SA_VERSION, SA_1_4 makeSuite = unittest.TestLoader().loadTestsFromTestCase -log = logging.getLogger('crate.testing.layer') +log = logging.getLogger() ch = logging.StreamHandler() ch.setLevel(logging.ERROR) log.addHandler(ch) @@ -73,46 +45,9 @@ def cprint(s): print(s) -settings = { - 'udc.enabled': 'false', - 'lang.js.enabled': 'true', - 'auth.host_based.enabled': 'true', - 'auth.host_based.config.0.user': 'crate', - 'auth.host_based.config.0.method': 'trust', - 'auth.host_based.config.98.user': 'trusted_me', - 'auth.host_based.config.98.method': 'trust', - 'auth.host_based.config.99.user': 'me', - 'auth.host_based.config.99.method': 'password', -} crate_layer = None -def ensure_cratedb_layer(): - """ - In order to skip individual tests by manually disabling them within - `def test_suite()`, it is crucial make the test layer not run on each - and every occasion. So, things like this will be possible:: - - ./bin/test -vvvv --ignore_dir=testing - - TODO: Through a subsequent patch, the possibility to individually - unselect specific tests might be added to `def test_suite()` - on behalf of environment variables. - A blueprint for this kind of logic can be found at - https://github.com/crate/crate/commit/414cd833. - """ - global crate_layer - - if crate_layer is None: - crate_layer = CrateLayer('crate', - crate_home=crate_path(), - port=crate_port, - host=localhost, - transport_port=crate_transport_port, - settings=settings) - return crate_layer - - def setUpCrateLayerBaseline(test): test.globs['crate_host'] = crate_host test.globs['pprint'] = pprint @@ -202,115 +137,6 @@ def tearDownDropEntitiesSqlAlchemy(test): _execute_statements(ddl_statements) -class HttpsTestServerLayer: - PORT = 65534 - HOST = "localhost" - CERT_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), - "pki/server_valid.pem")) - CACERT_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), - "pki/cacert_valid.pem")) - - __name__ = "httpsserver" - __bases__ = tuple() - - class HttpsServer(HTTPServer): - def get_request(self): - - # Prepare SSL context. - context = ssl._create_unverified_context( - protocol=ssl.PROTOCOL_TLS_SERVER, - cert_reqs=ssl.CERT_OPTIONAL, - check_hostname=False, - purpose=ssl.Purpose.CLIENT_AUTH, - certfile=HttpsTestServerLayer.CERT_FILE, - keyfile=HttpsTestServerLayer.CERT_FILE, - cafile=HttpsTestServerLayer.CACERT_FILE) - - # Set minimum protocol version, TLSv1 and TLSv1.1 are unsafe. - context.minimum_version = ssl.TLSVersion.TLSv1_2 - - # Wrap TLS encryption around socket. - socket, client_address = HTTPServer.get_request(self) - socket = context.wrap_socket(socket, server_side=True) - - return socket, client_address - - class HttpsHandler(BaseHTTPRequestHandler): - - payload = json.dumps({"name": "test", "status": 200, }) - - def do_GET(self): - self.send_response(200) - payload = self.payload.encode('UTF-8') - self.send_header("Content-Length", len(payload)) - self.send_header("Content-Type", "application/json; charset=UTF-8") - self.end_headers() - self.wfile.write(payload) - - def setUp(self): - self.server = self.HttpsServer( - (self.HOST, self.PORT), - self.HttpsHandler - ) - thread = threading.Thread(target=self.serve_forever) - thread.daemon = True # quit interpreter when only thread exists - thread.start() - self.waitForServer() - - def serve_forever(self): - print("listening on", self.HOST, self.PORT) - self.server.serve_forever() - print("server stopped.") - - def tearDown(self): - self.server.shutdown() - self.server.server_close() - - def isUp(self): - """ - Test if a host is up. - """ - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - ex = s.connect_ex((self.HOST, self.PORT)) - s.close() - return ex == 0 - - def waitForServer(self, timeout=5): - """ - Wait for the host to be available. - """ - with stopit.ThreadingTimeout(timeout) as to_ctx_mgr: - while True: - if self.isUp(): - break - time.sleep(0.001) - - if not to_ctx_mgr: - raise TimeoutError("Could not properly start embedded webserver " - "within {} seconds".format(timeout)) - - -def setUpWithHttps(test): - test.globs['crate_host'] = "https://{0}:{1}".format( - HttpsTestServerLayer.HOST, HttpsTestServerLayer.PORT - ) - test.globs['pprint'] = pprint - test.globs['print'] = cprint - - test.globs['cacert_valid'] = os.path.abspath( - os.path.join(os.path.dirname(__file__), "pki/cacert_valid.pem") - ) - test.globs['cacert_invalid'] = os.path.abspath( - os.path.join(os.path.dirname(__file__), "pki/cacert_invalid.pem") - ) - test.globs['clientcert_valid'] = os.path.abspath( - os.path.join(os.path.dirname(__file__), "pki/client_valid.pem") - ) - test.globs['clientcert_invalid'] = os.path.abspath( - os.path.join(os.path.dirname(__file__), "pki/client_invalid.pem") - ) - - def _execute_statements(statements, on_error="ignore"): with connect(crate_host) as conn: cursor = conn.cursor() @@ -337,9 +163,6 @@ def test_suite(): suite = unittest.TestSuite() flags = (doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS) - # Unit tests. - suite.addTest(sqlalchemy_test_suite_unit()) - sqlalchemy_integration_tests = [ 'docs/by-example/sqlalchemy/getting-started.rst', 'docs/by-example/sqlalchemy/crud.rst', @@ -363,8 +186,6 @@ def test_suite(): optionflags=flags, encoding='utf-8' ) - s.layer = ensure_cratedb_layer() - s.addTest(sqlalchemy_test_suite_integration()) suite.addTest(s) return suite diff --git a/src/crate/testing/__init__.py b/src/crate/testing/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/crate/testing/tests.py b/src/crate/testing/tests.py deleted file mode 100644 index 2a6e06d0..00000000 --- a/src/crate/testing/tests.py +++ /dev/null @@ -1,34 +0,0 @@ -# vi: set encoding=utf-8 -# -*- coding: utf-8; -*- -# -# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor -# license agreements. See the NOTICE file distributed with this work for -# additional information regarding copyright ownership. Crate licenses -# this file to you under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. You may -# obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -# However, if you have executed another commercial license agreement -# with Crate these terms will supersede the license and you may use the -# software solely pursuant to the terms of the relevant commercial agreement. - -import unittest -from .test_layer import LayerUtilsTest, LayerTest - - -makeSuite = unittest.TestLoader().loadTestsFromTestCase - - -def test_suite(): - suite = unittest.TestSuite() - suite.addTest(makeSuite(LayerUtilsTest)) - suite.addTest(makeSuite(LayerTest)) - return suite diff --git a/src/sqlalchemy_cratedb/__init__.py b/src/sqlalchemy_cratedb/__init__.py index 83bb4296..36198beb 100644 --- a/src/sqlalchemy_cratedb/__init__.py +++ b/src/sqlalchemy_cratedb/__init__.py @@ -1 +1,56 @@ -DUMMY = 42 +# -*- coding: utf-8; -*- +# +# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. Crate licenses +# this file to you under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. You may +# obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# However, if you have executed another commercial license agreement +# with Crate these terms will supersede the license and you may use the +# software solely pursuant to the terms of the relevant commercial agreement. + +from .compat.api13 import monkeypatch_add_exec_driver_sql +from .dialect import CrateDialect +from .sa_version import SA_1_4, SA_2_0, SA_VERSION +from .support import insert_bulk +from .types import Geopoint, Geoshape, ObjectArray, ObjectType + + +if SA_VERSION < SA_1_4: + import textwrap + import warnings + + # SQLAlchemy 1.3 is effectively EOL. + SA13_DEPRECATION_WARNING = textwrap.dedent(""" + WARNING: SQLAlchemy 1.3 is effectively EOL. + + SQLAlchemy 1.3 is EOL since 2023-01-27. + Future versions of the CrateDB SQLAlchemy dialect will drop support for SQLAlchemy 1.3. + It is recommended that you transition to using SQLAlchemy 1.4 or 2.0: + + - https://docs.sqlalchemy.org/en/14/changelog/migration_14.html + - https://docs.sqlalchemy.org/en/20/changelog/migration_20.html + """.lstrip("\n")) + warnings.warn(message=SA13_DEPRECATION_WARNING, category=DeprecationWarning) + + # SQLAlchemy 1.3 does not have the `exec_driver_sql` method, so add it. + monkeypatch_add_exec_driver_sql() + + +__all__ = [ + CrateDialect, + Geopoint, + Geoshape, + ObjectArray, + ObjectType, +] diff --git a/src/crate/client/__init__.py b/src/sqlalchemy_cratedb/compat/__init__.py similarity index 100% rename from src/crate/client/__init__.py rename to src/sqlalchemy_cratedb/compat/__init__.py diff --git a/src/crate/client/sqlalchemy/compat/api13.py b/src/sqlalchemy_cratedb/compat/api13.py similarity index 100% rename from src/crate/client/sqlalchemy/compat/api13.py rename to src/sqlalchemy_cratedb/compat/api13.py diff --git a/src/crate/client/sqlalchemy/compat/core10.py b/src/sqlalchemy_cratedb/compat/core10.py similarity index 99% rename from src/crate/client/sqlalchemy/compat/core10.py rename to src/sqlalchemy_cratedb/compat/core10.py index 92c62dd8..1dce6c74 100644 --- a/src/crate/client/sqlalchemy/compat/core10.py +++ b/src/sqlalchemy_cratedb/compat/core10.py @@ -28,7 +28,7 @@ _key_getters_for_crud_column, _scan_cols, _scan_insert_from_select_cols) -from crate.client.sqlalchemy.compiler import CrateCompiler +from sqlalchemy_cratedb.compiler import CrateCompiler class CrateCompilerSA10(CrateCompiler): diff --git a/src/crate/client/sqlalchemy/compat/core14.py b/src/sqlalchemy_cratedb/compat/core14.py similarity index 99% rename from src/crate/client/sqlalchemy/compat/core14.py rename to src/sqlalchemy_cratedb/compat/core14.py index 2dd6670a..a77da5bb 100644 --- a/src/crate/client/sqlalchemy/compat/core14.py +++ b/src/sqlalchemy_cratedb/compat/core14.py @@ -29,7 +29,7 @@ _key_getters_for_crud_column, _scan_cols, _scan_insert_from_select_cols) -from crate.client.sqlalchemy.compiler import CrateCompiler +from sqlalchemy_cratedb.compiler import CrateCompiler class CrateCompilerSA14(CrateCompiler): diff --git a/src/crate/client/sqlalchemy/compat/core20.py b/src/sqlalchemy_cratedb/compat/core20.py similarity index 99% rename from src/crate/client/sqlalchemy/compat/core20.py rename to src/sqlalchemy_cratedb/compat/core20.py index 6f128876..a3985092 100644 --- a/src/crate/client/sqlalchemy/compat/core20.py +++ b/src/sqlalchemy_cratedb/compat/core20.py @@ -37,7 +37,7 @@ from sqlalchemy.sql.dml import DMLState, _DMLColumnElement from sqlalchemy.sql.dml import isinsert as _compile_state_isinsert -from crate.client.sqlalchemy.compiler import CrateCompiler +from sqlalchemy_cratedb.compiler import CrateCompiler class CrateCompilerSA20(CrateCompiler): diff --git a/src/crate/client/sqlalchemy/compiler.py b/src/sqlalchemy_cratedb/compiler.py similarity index 100% rename from src/crate/client/sqlalchemy/compiler.py rename to src/sqlalchemy_cratedb/compiler.py diff --git a/src/crate/client/sqlalchemy/dialect.py b/src/sqlalchemy_cratedb/dialect.py similarity index 100% rename from src/crate/client/sqlalchemy/dialect.py rename to src/sqlalchemy_cratedb/dialect.py diff --git a/src/crate/client/sqlalchemy/predicates/__init__.py b/src/sqlalchemy_cratedb/predicates.py similarity index 100% rename from src/crate/client/sqlalchemy/predicates/__init__.py rename to src/sqlalchemy_cratedb/predicates.py diff --git a/src/crate/client/sqlalchemy/sa_version.py b/src/sqlalchemy_cratedb/sa_version.py similarity index 100% rename from src/crate/client/sqlalchemy/sa_version.py rename to src/sqlalchemy_cratedb/sa_version.py diff --git a/src/crate/client/sqlalchemy/support.py b/src/sqlalchemy_cratedb/support.py similarity index 100% rename from src/crate/client/sqlalchemy/support.py rename to src/sqlalchemy_cratedb/support.py diff --git a/src/crate/client/sqlalchemy/types.py b/src/sqlalchemy_cratedb/types.py similarity index 100% rename from src/crate/client/sqlalchemy/types.py rename to src/sqlalchemy_cratedb/types.py diff --git a/src/crate/client/sqlalchemy/tests/__init__.py b/tests/__init__.py similarity index 91% rename from src/crate/client/sqlalchemy/tests/__init__.py rename to tests/__init__.py index d6d37493..3584363a 100644 --- a/src/crate/client/sqlalchemy/tests/__init__.py +++ b/tests/__init__.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -from ..compat.api13 import monkeypatch_amend_select_sa14, monkeypatch_add_connectionfairy_driver_connection -from ..sa_version import SA_1_4, SA_VERSION -from ...test_util import ParametrizedTestCase +from sqlalchemy_cratedb.compat.api13 import monkeypatch_amend_select_sa14, monkeypatch_add_connectionfairy_driver_connection +from sqlalchemy_cratedb.sa_version import SA_1_4, SA_VERSION +from crate.client.test_util import ParametrizedTestCase # `sql.select()` of SQLAlchemy 1.3 uses old calling semantics, # but the test cases already need the modern ones. diff --git a/src/crate/client/sqlalchemy/tests/array_test.py b/tests/array_test.py similarity index 100% rename from src/crate/client/sqlalchemy/tests/array_test.py rename to tests/array_test.py diff --git a/src/crate/client/sqlalchemy/tests/bulk_test.py b/tests/bulk_test.py similarity index 98% rename from src/crate/client/sqlalchemy/tests/bulk_test.py rename to tests/bulk_test.py index 4546d1a4..75c1953b 100644 --- a/src/crate/client/sqlalchemy/tests/bulk_test.py +++ b/tests/bulk_test.py @@ -26,7 +26,7 @@ import sqlalchemy as sa from sqlalchemy.orm import Session -from crate.client.sqlalchemy.sa_version import SA_VERSION, SA_2_0, SA_1_4 +from sqlalchemy_cratedb import SA_VERSION, SA_2_0, SA_1_4 try: from sqlalchemy.orm import declarative_base @@ -177,7 +177,7 @@ def test_bulk_save_pandas(self, mock_cursor): Verify bulk INSERT with pandas. """ from pandas._testing import makeTimeDataFrame - from crate.client.sqlalchemy.support import insert_bulk + from sqlalchemy_cratedb import insert_bulk # 42 records / 8 chunksize = 5.25, which means 6 batches will be emitted. INSERT_RECORDS = 42 @@ -217,7 +217,7 @@ def test_bulk_save_dask(self, mock_cursor): """ import dask.dataframe as dd from pandas._testing import makeTimeDataFrame - from crate.client.sqlalchemy.support import insert_bulk + from sqlalchemy_cratedb import insert_bulk # 42 records / 4 partitions means each partition has a size of 10.5 elements. # Because the chunk size 8 is slightly smaller than 10, the partition will not diff --git a/src/crate/client/sqlalchemy/tests/compiler_test.py b/tests/compiler_test.py similarity index 98% rename from src/crate/client/sqlalchemy/tests/compiler_test.py rename to tests/compiler_test.py index 9c08154b..e280d6c6 100644 --- a/src/crate/client/sqlalchemy/tests/compiler_test.py +++ b/tests/compiler_test.py @@ -24,23 +24,23 @@ from unittest.mock import MagicMock, patch from crate.client.cursor import Cursor -from crate.client.sqlalchemy.compiler import crate_before_execute +from sqlalchemy_cratedb.compiler import crate_before_execute import sqlalchemy as sa from sqlalchemy.sql import text, Update -from crate.testing.util import ExtraAssertions +from tests.settings import crate_host +from tests.util import ExtraAssertions try: from sqlalchemy.orm import declarative_base except ImportError: from sqlalchemy.ext.declarative import declarative_base -from crate.client.sqlalchemy.sa_version import SA_VERSION, SA_1_4, SA_2_0 -from crate.client.sqlalchemy.types import ObjectType +from sqlalchemy_cratedb import SA_VERSION, SA_1_4, SA_2_0 +from sqlalchemy_cratedb import ObjectType from crate.client.test_util import ParametrizedTestCase -from crate.testing.settings import crate_host class SqlAlchemyCompilerTest(ParametrizedTestCase, ExtraAssertions): diff --git a/src/crate/client/sqlalchemy/tests/connection_test.py b/tests/connection_test.py similarity index 100% rename from src/crate/client/sqlalchemy/tests/connection_test.py rename to tests/connection_test.py diff --git a/src/crate/client/sqlalchemy/tests/create_table_test.py b/tests/create_table_test.py similarity index 98% rename from src/crate/client/sqlalchemy/tests/create_table_test.py rename to tests/create_table_test.py index 4c6072aa..f74c45ed 100644 --- a/src/crate/client/sqlalchemy/tests/create_table_test.py +++ b/tests/create_table_test.py @@ -18,14 +18,14 @@ # However, if you have executed another commercial license agreement # with Crate these terms will supersede the license and you may use the # software solely pursuant to the terms of the relevant commercial agreement. - +import pytest import sqlalchemy as sa try: from sqlalchemy.orm import declarative_base except ImportError: from sqlalchemy.ext.declarative import declarative_base -from crate.client.sqlalchemy.types import ObjectType, ObjectArray, Geopoint +from sqlalchemy_cratedb import ObjectType, ObjectArray, Geopoint from crate.client.cursor import Cursor from unittest import TestCase @@ -225,6 +225,7 @@ class DummyTable(self.Base): 'b INT, \n\t' 'PRIMARY KEY (pk)\n)\n\n'), ()) + @pytest.mark.skip("CompileError not raised") def test_column_geopoint_without_index(self): class DummyTable(self.Base): __tablename__ = 't' diff --git a/src/crate/client/sqlalchemy/tests/datetime_test.py b/tests/datetime_test.py similarity index 100% rename from src/crate/client/sqlalchemy/tests/datetime_test.py rename to tests/datetime_test.py diff --git a/src/crate/client/sqlalchemy/tests/dialect_test.py b/tests/dialect_test.py similarity index 97% rename from src/crate/client/sqlalchemy/tests/dialect_test.py rename to tests/dialect_test.py index bdcfc838..e797f0b5 100644 --- a/src/crate/client/sqlalchemy/tests/dialect_test.py +++ b/tests/dialect_test.py @@ -26,9 +26,9 @@ import sqlalchemy as sa from crate.client.cursor import Cursor -from crate.client.sqlalchemy import SA_VERSION -from crate.client.sqlalchemy.sa_version import SA_1_4, SA_2_0 -from crate.client.sqlalchemy.types import ObjectType +from sqlalchemy_cratedb import SA_VERSION +from sqlalchemy_cratedb import SA_1_4, SA_2_0 +from sqlalchemy_cratedb import ObjectType from sqlalchemy import inspect from sqlalchemy.orm import Session try: diff --git a/src/crate/client/sqlalchemy/tests/dict_test.py b/tests/dict_test.py similarity index 99% rename from src/crate/client/sqlalchemy/tests/dict_test.py rename to tests/dict_test.py index 9695882b..84b6f491 100644 --- a/src/crate/client/sqlalchemy/tests/dict_test.py +++ b/tests/dict_test.py @@ -31,7 +31,7 @@ except ImportError: from sqlalchemy.ext.declarative import declarative_base -from crate.client.sqlalchemy.types import ObjectArray, ObjectType +from sqlalchemy_cratedb import ObjectArray, ObjectType from crate.client.cursor import Cursor diff --git a/src/crate/client/sqlalchemy/tests/function_test.py b/tests/function_test.py similarity index 100% rename from src/crate/client/sqlalchemy/tests/function_test.py rename to tests/function_test.py diff --git a/src/crate/client/sqlalchemy/tests/insert_from_select_test.py b/tests/insert_from_select_test.py similarity index 100% rename from src/crate/client/sqlalchemy/tests/insert_from_select_test.py rename to tests/insert_from_select_test.py diff --git a/src/crate/client/sqlalchemy/tests/match_test.py b/tests/match_test.py similarity index 97% rename from src/crate/client/sqlalchemy/tests/match_test.py rename to tests/match_test.py index 735709c3..22127e8d 100644 --- a/src/crate/client/sqlalchemy/tests/match_test.py +++ b/tests/match_test.py @@ -30,8 +30,8 @@ except ImportError: from sqlalchemy.ext.declarative import declarative_base -from crate.client.sqlalchemy.types import ObjectType -from crate.client.sqlalchemy.predicates import match +from sqlalchemy_cratedb import ObjectType +from sqlalchemy_cratedb.predicates import match from crate.client.cursor import Cursor diff --git a/src/crate/client/sqlalchemy/tests/query_caching.py b/tests/query_caching.py similarity index 97% rename from src/crate/client/sqlalchemy/tests/query_caching.py rename to tests/query_caching.py index 43e28a44..4381f61c 100644 --- a/src/crate/client/sqlalchemy/tests/query_caching.py +++ b/tests/query_caching.py @@ -26,7 +26,7 @@ from sqlalchemy.orm import Session from sqlalchemy.sql.operators import eq -from crate.client.sqlalchemy import SA_VERSION, SA_1_4 +from sqlalchemy_cratedb import SA_VERSION, SA_1_4 from crate.testing.settings import crate_host try: @@ -34,7 +34,7 @@ except ImportError: from sqlalchemy.ext.declarative import declarative_base -from crate.client.sqlalchemy.types import ObjectType, ObjectArray +from sqlalchemy_cratedb import ObjectType, ObjectArray class SqlAlchemyQueryCompilationCaching(TestCase): diff --git a/src/crate/__init__.py b/tests/settings.py similarity index 82% rename from src/crate/__init__.py rename to tests/settings.py index bbe84413..2f0b0f1d 100644 --- a/src/crate/__init__.py +++ b/tests/settings.py @@ -1,3 +1,4 @@ +# vi: set encoding=utf-8 # -*- coding: utf-8; -*- # # Licensed to CRATE Technology GmbH ("Crate") under one or more contributor @@ -18,3 +19,10 @@ # However, if you have executed another commercial license agreement # with Crate these terms will supersede the license and you may use the # software solely pursuant to the terms of the relevant commercial agreement. +from __future__ import absolute_import + + +crate_port = 4200 +localhost = '127.0.0.1' +crate_host = "{host}:{port}".format(host=localhost, port=crate_port) +crate_uri = "http://%s" % crate_host diff --git a/tests/test_dummy.py b/tests/test_dummy.py deleted file mode 100644 index ffc2300b..00000000 --- a/tests/test_dummy.py +++ /dev/null @@ -1,5 +0,0 @@ -from sqlalchemy_cratedb import DUMMY - - -def test_dummy(): - assert DUMMY == 42 diff --git a/src/crate/client/sqlalchemy/tests/update_test.py b/tests/update_test.py similarity index 98% rename from src/crate/client/sqlalchemy/tests/update_test.py rename to tests/update_test.py index a2d5462b..5062f229 100644 --- a/src/crate/client/sqlalchemy/tests/update_test.py +++ b/tests/update_test.py @@ -23,7 +23,7 @@ from unittest import TestCase from unittest.mock import patch, MagicMock -from crate.client.sqlalchemy.types import ObjectType +from sqlalchemy_cratedb import ObjectType import sqlalchemy as sa from sqlalchemy.orm import Session diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 00000000..4acc7a0e --- /dev/null +++ b/tests/util.py @@ -0,0 +1,47 @@ +import unittest + + +class ExtraAssertions: + """ + Additional assert methods for unittest. + + - https://github.com/python/cpython/issues/71339 + - https://bugs.python.org/issue14819 + - https://bugs.python.org/file43047/extra_assertions.patch + """ + + def assertIsSubclass(self, cls, superclass, msg=None): + try: + r = issubclass(cls, superclass) + except TypeError: + if not isinstance(cls, type): + self.fail(self._formatMessage(msg, + '%r is not a class' % (cls,))) + raise + if not r: + self.fail(self._formatMessage(msg, + '%r is not a subclass of %r' % (cls, superclass))) + + +class ParametrizedTestCase(unittest.TestCase): + """ + TestCase classes that want to be parametrized should + inherit from this class. + + https://eli.thegreenplace.net/2011/08/02/python-unit-testing-parametrized-test-cases + """ + def __init__(self, methodName="runTest", param=None): + super(ParametrizedTestCase, self).__init__(methodName) + self.param = param + + @staticmethod + def parametrize(testcase_klass, param=None): + """ Create a suite containing all tests taken from the given + subclass, passing them the parameter 'param'. + """ + testloader = unittest.TestLoader() + testnames = testloader.getTestCaseNames(testcase_klass) + suite = unittest.TestSuite() + for name in testnames: + suite.addTest(testcase_klass(name, param=param)) + return suite diff --git a/src/crate/client/sqlalchemy/tests/warnings_test.py b/tests/warnings_test.py similarity index 86% rename from src/crate/client/sqlalchemy/tests/warnings_test.py rename to tests/warnings_test.py index 80023005..ede78709 100644 --- a/src/crate/client/sqlalchemy/tests/warnings_test.py +++ b/tests/warnings_test.py @@ -3,8 +3,8 @@ import warnings from unittest import TestCase, skipIf -from crate.client.sqlalchemy import SA_1_4, SA_VERSION -from crate.testing.util import ExtraAssertions +from sqlalchemy_cratedb import SA_1_4, SA_VERSION +from tests.util import ExtraAssertions class SqlAlchemyWarningsTest(TestCase, ExtraAssertions): @@ -27,8 +27,8 @@ def test_sa13_deprecation_warning(self): # Trigger a warning by importing the SQLAlchemy dialect module. # Because it already has been loaded, unload it beforehand. - del sys.modules["crate.client.sqlalchemy"] - import crate.client.sqlalchemy # noqa: F401 + del sys.modules["sqlalchemy_cratedb"] + import sqlalchemy_cratedb # noqa: F401 # Verify details of the SA13 EOL/deprecation warning. self.assertEqual(len(w), 1) @@ -44,7 +44,7 @@ def test_craty_object_deprecation_warning(self): with warnings.catch_warnings(record=True) as w: # Import the deprecated symbol. - from crate.client.sqlalchemy.types import Craty # noqa: F401 + from sqlalchemy_cratedb.types import Craty # noqa: F401 # Verify details of the deprecation warning. self.assertEqual(len(w), 1) @@ -55,7 +55,7 @@ def test_craty_object_deprecation_warning(self): with warnings.catch_warnings(record=True) as w: # Import the deprecated symbol. - from crate.client.sqlalchemy.types import Object # noqa: F401 + from sqlalchemy_cratedb.types import Object # noqa: F401 # Verify details of the deprecation warning. self.assertEqual(len(w), 1)