]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
improve test compat on windows and update gh workflows
authorFederico Caselli <cfederico87@gmail.com>
Mon, 20 Jan 2025 19:36:17 +0000 (20:36 +0100)
committerFederico Caselli <cfederico87@gmail.com>
Tue, 21 Jan 2025 21:28:03 +0000 (22:28 +0100)
Change-Id: Ia58a2db5310626ac77d7bae73fbb978f01194384

12 files changed:
.github/workflows/run-on-pr.yaml
.github/workflows/run-test.yaml
.pre-commit-config.yaml
alembic/context.pyi
alembic/runtime/environment.py
alembic/runtime/migration.py
alembic/testing/env.py
setup.cfg
tests/test_command.py
tests/test_script_production.py
tools/write_pyi.py
tox.ini

index ca6641e9429664fad0714d6aba5030c9c9bee989..d19e6606d81951de3c8721b2531b148cce80f996 100644 (file)
@@ -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
index 94853552fc0e4f0d838665b22ae404903d51251c..1407a663a20c2151b540b9d51d5b59072ec4aa50 100644 (file)
@@ -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
 
index ac4be8989c88690902175a350cdfbe12be75accc..7ee8e2b4f55e7ed886d9f688c75ecf55c21253d9 100644 (file)
@@ -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
 
index 80619fb24f13fadcbaa7fcd4a907b19f231b12e3..9117c31e8e3d7b4f2ee1badcc537b8346290edfe 100644 (file)
@@ -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
index a30972ec91251e4c38004ad138583789e5ffd89b..1ff71eef70ddc2f77853e06c09661c82a01741d4 100644 (file)
@@ -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
index 28f01c3b30df7b15ef47aa5f358b5a04c988a8dd..81cad3d5410e0e1610190362054b354eb553a17a 100644 (file)
@@ -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
index c37b4d303220056a650d8ff4f13a9c353aca2a9c..078798038890bb5690a22e61ba036364b59696e8 100644 (file)
@@ -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())
index 89c095f7af4d9f6bf34006a6788b7efceabf7056..63b2caf43ae5a24459ae3510081f7646ad1d8b95 100644 (file)
--- 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 =
index 04a624adec37361265880066deb3f2263487839d..1b41af6d96050d7d048ab7c2563ed32d673611a8 100644 (file)
@@ -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):
index a6618be2750bf5575c5deb72e5fde60131e88b8c..0551acb5bf9c19a085411c6965848ca173c72b30 100644 (file)
@@ -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)
 
index b2ad23001f38832364f33a4c82c4bf552edd80ee..1efe0ee79d063212e0b983980e868bd1166a7edb 100644 (file)
@@ -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 77439be377f29cf433760d89db6fea514f617bb1..931dc7400d39c3340436e9707084072d0af70b27 100644 (file)
--- 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