From: Federico Caselli Date: Mon, 20 Jan 2025 19:36:17 +0000 (+0100) Subject: improve test compat on windows and update gh workflows X-Git-Tag: rel_1_15_0~13 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=94f672c40fd1dd14346e2f66a7b7cffe2b7f50d9;p=thirdparty%2Fsqlalchemy%2Falembic.git improve test compat on windows and update gh workflows Change-Id: Ia58a2db5310626ac77d7bae73fbb978f01194384 --- diff --git a/.github/workflows/run-on-pr.yaml b/.github/workflows/run-on-pr.yaml index ca6641e9..d19e6606 100644 --- a/.github/workflows/run-on-pr.yaml +++ b/.github/workflows/run-on-pr.yaml @@ -23,12 +23,13 @@ jobs: # run this job using this matrix, excluding some combinations below. matrix: os: - - "ubuntu-latest" + - "ubuntu-22.04" python-version: - "3.11" sqlalchemy: - sqla13 - sqla14 + - sqla20 - sqlamain # abort all jobs as soon as one fails fail-fast: true diff --git a/.github/workflows/run-test.yaml b/.github/workflows/run-test.yaml index 94853552..1407a663 100644 --- a/.github/workflows/run-test.yaml +++ b/.github/workflows/run-test.yaml @@ -26,7 +26,7 @@ jobs: # run this job using this matrix, excluding some combinations below. matrix: os: - - "ubuntu-latest" + - "ubuntu-22.04" - "windows-latest" - "macos-latest" python-version: @@ -35,14 +35,24 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" sqlalchemy: - sqla13 - sqla14 + - sqla20 - sqlamain exclude: - # sqla13 does not seem to support 3.12 + # sqla13 does not support 3.12+ - sqlalchemy: sqla13 python-version: "3.12" + - sqlalchemy: sqla13 + python-version: "3.13" + # sqla14 does not support 3.13+ + - sqlalchemy: sqla14 + python-version: "3.13" + # sqlamain does not support 3.8 + - sqlalchemy: sqlamain + python-version: "3.8" fail-fast: false @@ -77,6 +87,7 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" fail-fast: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ac4be898..7ee8e2b4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/python/black - rev: 24.1.1 + rev: 24.10.0 hooks: - id: black diff --git a/alembic/context.pyi b/alembic/context.pyi index 80619fb2..9117c31e 100644 --- a/alembic/context.pyi +++ b/alembic/context.pyi @@ -5,7 +5,6 @@ from __future__ import annotations from typing import Any from typing import Callable from typing import Collection -from typing import ContextManager from typing import Dict from typing import Iterable from typing import List @@ -20,6 +19,8 @@ from typing import Tuple from typing import TYPE_CHECKING from typing import Union +from typing_extensions import ContextManager + if TYPE_CHECKING: from sqlalchemy.engine.base import Connection from sqlalchemy.engine.url import URL @@ -40,7 +41,9 @@ if TYPE_CHECKING: ### end imports ### -def begin_transaction() -> Union[_ProxyTransaction, ContextManager[None]]: +def begin_transaction() -> ( + Union[_ProxyTransaction, ContextManager[None, Optional[bool]]] +): """Return a context manager that will enclose an operation within a "transaction", as defined by the environment's offline diff --git a/alembic/runtime/environment.py b/alembic/runtime/environment.py index a30972ec..1ff71eef 100644 --- a/alembic/runtime/environment.py +++ b/alembic/runtime/environment.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Any from typing import Callable from typing import Collection -from typing import ContextManager from typing import Dict from typing import List from typing import Mapping @@ -18,6 +17,7 @@ from typing import Union from sqlalchemy.sql.schema import Column from sqlalchemy.sql.schema import FetchedValue +from typing_extensions import ContextManager from typing_extensions import Literal from .migration import _ProxyTransaction @@ -976,7 +976,7 @@ class EnvironmentContext(util.ModuleClsProxy): def begin_transaction( self, - ) -> Union[_ProxyTransaction, ContextManager[None]]: + ) -> Union[_ProxyTransaction, ContextManager[None, Optional[bool]]]: """Return a context manager that will enclose an operation within a "transaction", as defined by the environment's offline diff --git a/alembic/runtime/migration.py b/alembic/runtime/migration.py index 28f01c3b..81cad3d5 100644 --- a/alembic/runtime/migration.py +++ b/alembic/runtime/migration.py @@ -11,7 +11,6 @@ from typing import Any from typing import Callable from typing import cast from typing import Collection -from typing import ContextManager from typing import Dict from typing import Iterable from typing import Iterator @@ -27,6 +26,7 @@ from sqlalchemy import literal_column from sqlalchemy.engine import Engine from sqlalchemy.engine import url as sqla_url from sqlalchemy.engine.strategies import MockEngineStrategy +from typing_extensions import ContextManager from .. import ddl from .. import util @@ -368,7 +368,7 @@ class MigrationContext: def begin_transaction( self, _per_migration: bool = False - ) -> Union[_ProxyTransaction, ContextManager[None]]: + ) -> Union[_ProxyTransaction, ContextManager[None, Optional[bool]]]: """Begin a logical transaction for migration operations. This method is used within an ``env.py`` script to demarcate where diff --git a/alembic/testing/env.py b/alembic/testing/env.py index c37b4d30..07879803 100644 --- a/alembic/testing/env.py +++ b/alembic/testing/env.py @@ -1,5 +1,6 @@ import importlib.machinery import os +from pathlib import Path import shutil import textwrap @@ -16,7 +17,7 @@ from ..script import ScriptDirectory def _get_staging_directory(): if provision.FOLLOWER_IDENT: - return "scratch_%s" % provision.FOLLOWER_IDENT + return f"scratch_{provision.FOLLOWER_IDENT}" else: return "scratch" @@ -24,7 +25,7 @@ def _get_staging_directory(): def staging_env(create=True, template="generic", sourceless=False): cfg = _testing_config() if create: - path = os.path.join(_get_staging_directory(), "scripts") + path = _join_path(_get_staging_directory(), "scripts") assert not os.path.exists(path), ( "staging directory %s already exists; poor cleanup?" % path ) @@ -47,7 +48,7 @@ def staging_env(create=True, template="generic", sourceless=False): "pep3147_everything", ), sourceless make_sourceless( - os.path.join(path, "env.py"), + _join_path(path, "env.py"), "pep3147" if "pep3147" in sourceless else "simple", ) @@ -63,14 +64,14 @@ def clear_staging_env(): def script_file_fixture(txt): - dir_ = os.path.join(_get_staging_directory(), "scripts") - path = os.path.join(dir_, "script.py.mako") + dir_ = _join_path(_get_staging_directory(), "scripts") + path = _join_path(dir_, "script.py.mako") with open(path, "w") as f: f.write(txt) def env_file_fixture(txt): - dir_ = os.path.join(_get_staging_directory(), "scripts") + dir_ = _join_path(_get_staging_directory(), "scripts") txt = ( """ from alembic import context @@ -80,7 +81,7 @@ config = context.config + txt ) - path = os.path.join(dir_, "env.py") + path = _join_path(dir_, "env.py") pyc_path = util.pyc_file_from_path(path) if pyc_path: os.unlink(pyc_path) @@ -90,7 +91,7 @@ config = context.config def _sqlite_file_db(tempname="foo.db", future=False, scope=None, **options): - dir_ = os.path.join(_get_staging_directory(), "scripts") + dir_ = _join_path(_get_staging_directory(), "scripts") url = "sqlite:///%s/%s" % (dir_, tempname) if scope and util.sqla_14: options["scope"] = scope @@ -98,18 +99,18 @@ def _sqlite_file_db(tempname="foo.db", future=False, scope=None, **options): def _sqlite_testing_config(sourceless=False, future=False): - dir_ = os.path.join(_get_staging_directory(), "scripts") - url = "sqlite:///%s/foo.db" % dir_ + dir_ = _join_path(_get_staging_directory(), "scripts") + url = f"sqlite:///{dir_}/foo.db" sqlalchemy_future = future or ("future" in config.db.__class__.__module__) return _write_config_file( - """ + f""" [alembic] -script_location = %s -sqlalchemy.url = %s -sourceless = %s -%s +script_location = {dir_} +sqlalchemy.url = {url} +sourceless = {"true" if sourceless else "false"} +{"sqlalchemy.future = true" if sqlalchemy_future else ""} [loggers] keys = root,sqlalchemy @@ -140,29 +141,24 @@ keys = generic format = %%(levelname)-5.5s [%%(name)s] %%(message)s datefmt = %%H:%%M:%%S """ - % ( - dir_, - url, - "true" if sourceless else "false", - "sqlalchemy.future = true" if sqlalchemy_future else "", - ) ) def _multi_dir_testing_config(sourceless=False, extra_version_location=""): - dir_ = os.path.join(_get_staging_directory(), "scripts") + dir_ = _join_path(_get_staging_directory(), "scripts") sqlalchemy_future = "future" in config.db.__class__.__module__ url = "sqlite:///%s/foo.db" % dir_ return _write_config_file( - """ + f""" [alembic] -script_location = %s -sqlalchemy.url = %s -sqlalchemy.future = %s -sourceless = %s -version_locations = %%(here)s/model1/ %%(here)s/model2/ %%(here)s/model3/ %s +script_location = {dir_} +sqlalchemy.url = {url} +sqlalchemy.future = {"true" if sqlalchemy_future else "false"} +sourceless = {"true" if sourceless else "false"} +version_locations = %(here)s/model1/ %(here)s/model2/ %(here)s/model3/ \ +{extra_version_location} [loggers] keys = root @@ -188,26 +184,19 @@ keys = generic format = %%(levelname)-5.5s [%%(name)s] %%(message)s datefmt = %%H:%%M:%%S """ - % ( - dir_, - url, - "true" if sqlalchemy_future else "false", - "true" if sourceless else "false", - extra_version_location, - ) ) def _no_sql_testing_config(dialect="postgresql", directives=""): """use a postgresql url with no host so that connections guaranteed to fail""" - dir_ = os.path.join(_get_staging_directory(), "scripts") + dir_ = _join_path(_get_staging_directory(), "scripts") return _write_config_file( - """ + f""" [alembic] -script_location = %s -sqlalchemy.url = %s:// -%s +script_location ={dir_} +sqlalchemy.url = {dialect}:// +{directives} [loggers] keys = root @@ -234,7 +223,6 @@ format = %%(levelname)-5.5s [%%(name)s] %%(message)s datefmt = %%H:%%M:%%S """ - % (dir_, dialect, directives) ) @@ -250,7 +238,7 @@ def _testing_config(): if not os.access(_get_staging_directory(), os.F_OK): os.mkdir(_get_staging_directory()) - return Config(os.path.join(_get_staging_directory(), "test_alembic.ini")) + return Config(_join_path(_get_staging_directory(), "test_alembic.ini")) def write_script( @@ -270,9 +258,7 @@ def write_script( script = Script._from_path(scriptdir, path) old = scriptdir.revision_map.get_revision(script.revision) if old.down_revision != script.down_revision: - raise Exception( - "Can't change down_revision " "on a refresh operation." - ) + raise Exception("Can't change down_revision on a refresh operation.") scriptdir.revision_map.add_revision(script, _replace=True) if sourceless: @@ -312,9 +298,9 @@ def three_rev_fixture(cfg): write_script( script, a, - """\ + f"""\ "Rev A" -revision = '%s' +revision = '{a}' down_revision = None from alembic import op @@ -327,8 +313,7 @@ def upgrade(): def downgrade(): op.execute("DROP STEP 1") -""" - % a, +""", ) script.generate_revision(b, "revision b", refresh=True, head=a) @@ -358,10 +343,10 @@ def downgrade(): write_script( script, c, - """\ + f"""\ "Rev C" -revision = '%s' -down_revision = '%s' +revision = '{c}' +down_revision = '{b}' from alembic import op @@ -373,8 +358,7 @@ def upgrade(): def downgrade(): op.execute("DROP STEP 3") -""" - % (c, b), +""", ) return a, b, c @@ -396,10 +380,10 @@ def multi_heads_fixture(cfg, a, b, c): write_script( script, d, - """\ + f"""\ "Rev D" -revision = '%s' -down_revision = '%s' +revision = '{d}' +down_revision = '{b}' from alembic import op @@ -411,8 +395,7 @@ def upgrade(): def downgrade(): op.execute("DROP STEP 4") -""" - % (d, b), +""", ) script.generate_revision( @@ -421,10 +404,10 @@ def downgrade(): write_script( script, e, - """\ + f"""\ "Rev E" -revision = '%s' -down_revision = '%s' +revision = '{e}' +down_revision = '{d}' from alembic import op @@ -436,8 +419,7 @@ def upgrade(): def downgrade(): op.execute("DROP STEP 5") -""" - % (e, d), +""", ) script.generate_revision( @@ -446,10 +428,10 @@ def downgrade(): write_script( script, f, - """\ + f"""\ "Rev F" -revision = '%s' -down_revision = '%s' +revision = '{f}' +down_revision = '{b}' from alembic import op @@ -461,8 +443,7 @@ def upgrade(): def downgrade(): op.execute("DROP STEP 6") -""" - % (f, b), +""", ) return d, e, f @@ -471,25 +452,25 @@ def downgrade(): def _multidb_testing_config(engines): """alembic.ini fixture to work exactly with the 'multidb' template""" - dir_ = os.path.join(_get_staging_directory(), "scripts") + dir_ = _join_path(_get_staging_directory(), "scripts") sqlalchemy_future = "future" in config.db.__class__.__module__ databases = ", ".join(engines.keys()) engines = "\n\n".join( - "[%s]\n" "sqlalchemy.url = %s" % (key, value.url) + f"[{key}]\nsqlalchemy.url = {value.url}" for key, value in engines.items() ) return _write_config_file( - """ + f""" [alembic] -script_location = %s +script_location = {dir_} sourceless = false -sqlalchemy.future = %s -databases = %s +sqlalchemy.future = {"true" if sqlalchemy_future else "false"} +databases = {databases} -%s +{engines} [loggers] keys = root @@ -514,5 +495,8 @@ keys = generic format = %%(levelname)-5.5s [%%(name)s] %%(message)s datefmt = %%H:%%M:%%S """ - % (dir_, "true" if sqlalchemy_future else "false", databases, engines) ) + + +def _join_path(base: str, *more: str): + return str(Path(base).joinpath(*more).as_posix()) diff --git a/setup.cfg b/setup.cfg index 89c095f7..63b2caf4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,7 @@ install_requires = Mako importlib-metadata;python_version<"3.9" importlib-resources;python_version<"3.9" - typing-extensions>=4 + typing-extensions>=4.12 [options.extras_require] tz = diff --git a/tests/test_command.py b/tests/test_command.py index 04a624ad..1b41af6d 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -612,20 +612,22 @@ class CheckTest(TestBase): def _env_fixture(self, version_table_pk=True): env_file_fixture( - """ + f""" from sqlalchemy import MetaData, engine_from_config target_metadata = MetaData() engine = engine_from_config( config.get_section(config.config_ini_section), - prefix='sqlalchemy.') + prefix='sqlalchemy.' +) connection = engine.connect() context.configure( - connection=connection, target_metadata=target_metadata, - version_table_pk=%r + connection=connection, + target_metadata=target_metadata, + version_table_pk={version_table_pk} ) try: @@ -636,7 +638,6 @@ finally: engine.dispose() """ - % (version_table_pk,) ) def test_check_no_changes(self): diff --git a/tests/test_script_production.py b/tests/test_script_production.py index a6618be2..0551acb5 100644 --- a/tests/test_script_production.py +++ b/tests/test_script_production.py @@ -1377,7 +1377,7 @@ class NormPathTest(TestBase): script = ScriptDirectory.from_config(config) def normpath(path): - return path.replace("/", ":NORM:") + return path.replace(os.pathsep, ":NORM:") normpath = mock.Mock(side_effect=normpath) @@ -1389,7 +1389,7 @@ class NormPathTest(TestBase): os.path.join( _get_staging_directory(), "scripts", "versions" ) - ).replace("/", ":NORM:"), + ).replace(os.pathsep, ":NORM:"), ), ) @@ -1399,7 +1399,7 @@ class NormPathTest(TestBase): os.path.join( _get_staging_directory(), "scripts", "versions" ) - ).replace("/", ":NORM:"), + ).replace(os.pathsep, ":NORM:"), ) def test_script_location_multiple(self): @@ -1408,7 +1408,7 @@ class NormPathTest(TestBase): script = ScriptDirectory.from_config(config) def _normpath(path): - return path.replace("/", ":NORM:") + return path.replace(os.pathsep, ":NORM:") normpath = mock.Mock(side_effect=_normpath) diff --git a/tools/write_pyi.py b/tools/write_pyi.py index b2ad2300..1efe0ee7 100644 --- a/tools/write_pyi.py +++ b/tools/write_pyi.py @@ -218,6 +218,16 @@ def _generate_stub_for_meth( string_prefix = "r" if has_docs and chr(92) in fn_doc else "" if has_docs: noqua = " # noqa: E501" if file_info.docs_noqa_E501 else "" + + if sys.version_info >= (3, 13): + # python 3.13 seems to remove the leading spaces from docs, + # but the following needs them, so re-add it + # https://docs.python.org/3/whatsnew/3.13.html#other-language-changes + indent = " " + fn_doc = textwrap.indent(fn_doc, indent)[len(indent) :] + if fn_doc[-1] == "\n": + fn_doc += indent + docs = f'{string_prefix}"""{fn_doc}"""{noqua}' else: docs = "" diff --git a/tox.ini b/tox.ini index 77439be3..931dc740 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ deps=pytest>4.6,<8.2 backports.zoneinfo;python_version<"3.9" tzdata zimports - black==24.1.1 + black==24.10.0 greenlet>=1 @@ -97,7 +97,7 @@ deps= pydocstyle<4.0.0 # used by flake8-rst-docstrings pygments - black==24.1.1 + black==24.10.0 commands = flake8 ./alembic/ ./tests/ setup.py docs/build/conf.py {posargs} black --check setup.py tests alembic @@ -108,5 +108,5 @@ deps= sqlalchemy>=2 mako zimports - black==24.1.1 + black==24.10.0 commands = python tools/write_pyi.py