]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
drop py39 support
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 11 Oct 2025 14:28:35 +0000 (10:28 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 11 Oct 2025 16:49:30 +0000 (12:49 -0400)
Change-Id: I89a636ac3e687240ffb464acb8ade5055bba408b

15 files changed:
alembic/__init__.py
alembic/script/base.py
alembic/script/write_hooks.py
alembic/templates/async/alembic.ini.mako
alembic/templates/generic/alembic.ini.mako
alembic/templates/multidb/alembic.ini.mako
alembic/templates/pyproject/pyproject.toml.mako
alembic/templates/pyproject_async/pyproject.toml.mako
alembic/testing/fixtures.py
alembic/util/compat.py
alembic/util/pyfiles.py
docs/build/changelog.rst
docs/build/unreleased/remove_py39.rst [new file with mode: 0644]
noxfile.py
pyproject.toml

index c3ae61d2076ec5d7db8272a221bc675cdbb35846..7685cfa6a7bf5d00ad82197b22c509ac15adc5ea 100644 (file)
@@ -1,4 +1,4 @@
 from . import context
 from . import op
 
-__version__ = "1.16.6"
+__version__ = "1.17.0"
index 94292316bc8e9fabc9160487239dcb93d84dbe98..6b9692c63cb3705afaeb589934756b20f70b13b8 100644 (file)
@@ -38,12 +38,8 @@ if TYPE_CHECKING:
     from ..runtime.migration import StampStep
 
 try:
-    if compat.py39:
-        from zoneinfo import ZoneInfo
-        from zoneinfo import ZoneInfoNotFoundError
-    else:
-        from backports.zoneinfo import ZoneInfo  # type: ignore[import-not-found,no-redef] # noqa: E501
-        from backports.zoneinfo import ZoneInfoNotFoundError  # type: ignore[no-redef] # noqa: E501
+    from zoneinfo import ZoneInfo
+    from zoneinfo import ZoneInfoNotFoundError
 except ImportError:
     ZoneInfo = None  # type: ignore[assignment, misc]
 
index f40bb35f6a8c8878fe4fe4181f235e9fc010b36b..6b8161dbb2c30887fefd1d7c30aa319665c5f0b1 100644 (file)
@@ -10,11 +10,7 @@ import subprocess
 import sys
 from typing import Any
 from typing import Callable
-from typing import Dict
-from typing import List
-from typing import Optional
 from typing import TYPE_CHECKING
-from typing import Union
 
 from .. import util
 from ..util import compat
@@ -49,7 +45,7 @@ def register(name: str) -> Callable:
 
 def _invoke(
     name: str,
-    revision_path: Union[str, os.PathLike[str]],
+    revision_path: str | os.PathLike[str],
     options: PostWriteHookConfig,
 ) -> Any:
     """Invokes the formatter registered for the given name.
@@ -72,7 +68,7 @@ def _invoke(
 
 
 def _run_hooks(
-    path: Union[str, os.PathLike[str]], hooks: list[PostWriteHookConfig]
+    path: str | os.PathLike[str], hooks: list[PostWriteHookConfig]
 ) -> None:
     """Invoke hooks for a generated revision."""
 
@@ -92,7 +88,7 @@ def _run_hooks(
                 _invoke(type_, path, hook)
 
 
-def _parse_cmdline_options(cmdline_options_str: str, path: str) -> List[str]:
+def _parse_cmdline_options(cmdline_options_str: str, path: str) -> list[str]:
     """Parse options from a string into a list.
 
     Also substitutes the revision script token with the actual filename of
@@ -124,13 +120,13 @@ def _get_required_option(options: dict, name: str) -> str:
 
 
 def _run_hook(
-    path: str, options: dict, ignore_output: bool, command: List[str]
+    path: str, options: dict, ignore_output: bool, command: list[str]
 ) -> None:
-    cwd: Optional[str] = options.get("cwd", None)
+    cwd: str | None = options.get("cwd", None)
     cmdline_options_str = options.get("options", "")
     cmdline_options_list = _parse_cmdline_options(cmdline_options_str, path)
 
-    kw: Dict[str, Any] = {}
+    kw: dict[str, Any] = {}
     if ignore_output:
         kw["stdout"] = kw["stderr"] = subprocess.DEVNULL
 
index 67acc6d05426f6b03b8c650ff79c70ab2216b1b1..62617c4c237a34f5e28ba792188c8ada684c8d67 100644 (file)
@@ -20,8 +20,8 @@ prepend_sys_path = .
 
 # timezone to use when rendering the date within the migration file
 # as well as the filename.
-# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
-# Any required deps can installed by adding `alembic[tz]` to the pip requirements
+# If specified, requires the tzdata library which can be installed by adding
+# `alembic[tz]` to the pip requirements.
 # string value is passed to ZoneInfo()
 # leave blank for localtime
 # timezone =
index bb93d0e3cf17447b5aa78da5e1d54ba4d966bae6..eb8c063fcdda95f68d17a743c93166d7308ca244 100644 (file)
@@ -21,8 +21,8 @@ prepend_sys_path = .
 
 # timezone to use when rendering the date within the migration file
 # as well as the filename.
-# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
-# Any required deps can installed by adding `alembic[tz]` to the pip requirements
+# If specified, requires the tzdata library which can be installed by adding
+# `alembic[tz]` to the pip requirements.
 # string value is passed to ZoneInfo()
 # leave blank for localtime
 # timezone =
index a6629839552197a124088e8e46a011446b5165ee..f4b65e6201c0f468308d35b504e084557dfbbe3c 100644 (file)
@@ -20,8 +20,8 @@ prepend_sys_path = .
 
 # timezone to use when rendering the date within the migration file
 # as well as the filename.
-# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
-# Any required deps can installed by adding `alembic[tz]` to the pip requirements
+# If specified, requires the tzdata library which can be installed by adding
+# `alembic[tz]` to the pip requirements.
 # string value is passed to ZoneInfo()
 # leave blank for localtime
 # timezone =
index e68cef331352ba8cd12bf1a4c9432c526c20ecec..224faec1e36e98e58dbb5aa88aee390298660403 100644 (file)
@@ -19,8 +19,8 @@ prepend_sys_path = [
 
 # timezone to use when rendering the date within the migration file
 # as well as the filename.
-# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
-# Any required deps can installed by adding `alembic[tz]` to the pip requirements
+# If specified, requires the tzdata library which can be installed by adding
+# `alembic[tz]` to the pip requirements.
 # string value is passed to ZoneInfo()
 # leave blank for localtime
 # timezone =
index e68cef331352ba8cd12bf1a4c9432c526c20ecec..224faec1e36e98e58dbb5aa88aee390298660403 100644 (file)
@@ -19,8 +19,8 @@ prepend_sys_path = [
 
 # timezone to use when rendering the date within the migration file
 # as well as the filename.
-# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
-# Any required deps can installed by adding `alembic[tz]` to the pip requirements
+# If specified, requires the tzdata library which can be installed by adding
+# `alembic[tz]` to the pip requirements.
 # string value is passed to ZoneInfo()
 # leave blank for localtime
 # timezone =
index 61bcd7e938cce3d1916056eeeca91eb476bbcbbb..6d801fcbc1c0252442d1f83e90b858853bbbcb49 100644 (file)
@@ -8,6 +8,9 @@ import re
 import shutil
 from typing import Any
 from typing import Dict
+from typing import Generator
+from typing import Literal
+from typing import overload
 
 from sqlalchemy import Column
 from sqlalchemy import create_mock_engine
@@ -52,7 +55,7 @@ class TestBase(SQLAlchemyTestBase):
                 shutil.rmtree(file_path)
 
     @contextmanager
-    def pushd(self, dirname):
+    def pushd(self, dirname) -> Generator[None, None, None]:
         current_dir = os.getcwd()
         try:
             os.chdir(dirname)
@@ -108,8 +111,24 @@ def capture_db(dialect="postgresql://"):
 _engs: Dict[Any, Any] = {}
 
 
+@overload
 @contextmanager
-def capture_context_buffer(**kw):
+def capture_context_buffer(
+    bytes_io: Literal[True], **kw: Any
+) -> Generator[io.BytesIO, None, None]: ...
+
+
+@overload
+@contextmanager
+def capture_context_buffer(
+    **kw: Any,
+) -> Generator[io.StringIO, None, None]: ...
+
+
+@contextmanager
+def capture_context_buffer(
+    **kw: Any,
+) -> Generator[io.StringIO | io.BytesIO, None, None]:
     if kw.pop("bytes_io", False):
         buf = io.BytesIO()
     else:
@@ -127,7 +146,9 @@ def capture_context_buffer(**kw):
 
 
 @contextmanager
-def capture_engine_context_buffer(**kw):
+def capture_engine_context_buffer(
+    **kw: Any,
+) -> Generator[io.StringIO, None, None]:
     from .env import _sqlite_file_db
     from sqlalchemy import event
 
index 131f16a00296ed16950151a32f712fb6fdeaf9a5..527ffdc97d2351cfd2c43355e4fd9af0bd00d2e3 100644 (file)
@@ -3,6 +3,8 @@
 from __future__ import annotations
 
 from configparser import ConfigParser
+from importlib import metadata
+from importlib.metadata import EntryPoint
 import io
 import os
 from pathlib import Path
@@ -10,10 +12,7 @@ import sys
 import typing
 from typing import Any
 from typing import Iterator
-from typing import List
-from typing import Optional
 from typing import Sequence
-from typing import Union
 
 if True:
     # zimports hack for too-long names
@@ -30,8 +29,6 @@ py314 = sys.version_info >= (3, 14)
 py313 = sys.version_info >= (3, 13)
 py312 = sys.version_info >= (3, 12)
 py311 = sys.version_info >= (3, 11)
-py310 = sys.version_info >= (3, 10)
-py39 = sys.version_info >= (3, 9)
 
 
 # produce a wrapper that allows encoded text to stream
@@ -42,19 +39,6 @@ class EncodedIO(io.TextIOWrapper):
         pass
 
 
-if py39:
-    from importlib import resources as _resources
-
-    importlib_resources = _resources
-    from importlib import metadata as _metadata
-
-    importlib_metadata = _metadata
-    from importlib.metadata import EntryPoint as EntryPoint
-else:
-    import importlib_resources  # type:ignore # noqa
-    import importlib_metadata  # type:ignore # noqa
-    from importlib_metadata import EntryPoint  # type:ignore # noqa
-
 if py311:
     import tomllib as tomllib
 else:
@@ -109,15 +93,18 @@ else:
 
 
 def importlib_metadata_get(group: str) -> Sequence[EntryPoint]:
-    ep = importlib_metadata.entry_points()
-    if hasattr(ep, "select"):
-        return ep.select(group=group)
-    else:
-        return ep.get(group, ())  # type: ignore
+    """provide a facade for metadata.entry_points().
+
+    This is no longer a "compat" function as of Python 3.10, however
+    the function is widely referenced in the test suite and elsewhere so is
+    still in this module for compatibility reasons.
+
+    """
+    return metadata.entry_points().select(group=group)
 
 
 def formatannotation_fwdref(
-    annotation: Any, base_module: Optional[Any] = None
+    annotation: Any, base_module: Any | None = None
 ) -> str:
     """vendored from python 3.7"""
     # copied over _formatannotation from sqlalchemy 2.0
@@ -138,9 +125,6 @@ def formatannotation_fwdref(
 
 def read_config_parser(
     file_config: ConfigParser,
-    file_argument: Sequence[Union[str, os.PathLike[str]]],
-) -> List[str]:
-    if py310:
-        return file_config.read(file_argument, encoding="locale")
-    else:
-        return file_config.read(file_argument)
+    file_argument: list[str | os.PathLike[str]],
+) -> list[str]:
+    return file_config.read(file_argument, encoding="locale")
index 6b75d57792384b890a44d4c096e9a617254e7344..135a42dce24fc0cf9a42f247adcbe4b92cc72ac1 100644 (file)
@@ -3,6 +3,7 @@ from __future__ import annotations
 import atexit
 from contextlib import ExitStack
 import importlib
+from importlib import resources
 import importlib.machinery
 import importlib.util
 import os
@@ -17,7 +18,6 @@ from typing import Union
 from mako import exceptions
 from mako.template import Template
 
-from . import compat
 from .exc import CommandError
 
 
@@ -68,11 +68,11 @@ def coerce_resource_to_filename(fname_or_resource: str) -> pathlib.Path:
         file_manager = ExitStack()
         atexit.register(file_manager.close)
 
-        ref = compat.importlib_resources.files(tokens[0])
+        ref = resources.files(tokens[0])
         for tok in tokens[1:]:
             ref = ref / tok
         fname_or_resource = file_manager.enter_context(  # type: ignore[assignment]  # noqa: E501
-            compat.importlib_resources.as_file(ref)
+            resources.as_file(ref)
         )
     return pathlib.Path(fname_or_resource)
 
index 2ed6d2265137a8672c9d3f571024b6aa2846bf60..0bd8d88bed70d54a35f219a25d9eb2b4aa2286b1 100644 (file)
@@ -4,7 +4,7 @@ Changelog
 ==========
 
 .. changelog::
-    :version: 1.16.6
+    :version: 1.17.0
     :include_notes_from: unreleased
 
 .. changelog::
diff --git a/docs/build/unreleased/remove_py39.rst b/docs/build/unreleased/remove_py39.rst
new file mode 100644 (file)
index 0000000..4c300bb
--- /dev/null
@@ -0,0 +1,4 @@
+.. change::
+    :tags: change, general
+
+    The minimum Python version is now 3.10, as Python 3.9 is EOL.
index 7ce6a609f1fd0097f7acff8d1685b96764976835..3ffc096feb392ec24f4a2353d87c6f038fe80c1b 100644 (file)
@@ -2,7 +2,9 @@
 
 from __future__ import annotations
 
+from glob import glob
 import os
+import shutil
 import sys
 from typing import Optional
 from typing import Sequence
@@ -22,8 +24,6 @@ SQLA_REPO = os.environ.get(
 )
 
 PYTHON_VERSIONS = [
-    "3.8",
-    "3.9",
     "3.10",
     "3.11",
     "3.12",
@@ -48,7 +48,7 @@ def filter_sqla(
     if sqlalchemy == "sqla14":
         return python_version < parse_version("3.14")
     elif sqlalchemy == "sqlamain":
-        return python_version > parse_version("3.9")
+        return python_version >= parse_version("3.10")
     else:
         return True
 
@@ -176,6 +176,11 @@ def _tests(
             if database in ["oracle", "mssql"]:
                 session.run("python", "reap_dbs.py", "db_idents.txt")
 
+        # Clean up scratch directories
+        for scratch_dir in glob("scratch*"):
+            if os.path.isdir(scratch_dir):
+                shutil.rmtree(scratch_dir)
+
 
 @nox.session(name="pep484")
 def mypy_check(session: nox.Session) -> None:
index 3b9bed6c9778d146b1d82f65c2c07049388d5479..48c42569b864356028056f333d5dff636d5dee1b 100644 (file)
@@ -16,7 +16,6 @@ classifiers = [
     "Operating System :: OS Independent",
     "Programming Language :: Python",
     "Programming Language :: Python :: 3",
-    "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
@@ -25,7 +24,7 @@ classifiers = [
     "Programming Language :: Python :: Implementation :: PyPy",
     "Topic :: Database :: Front-Ends",
 ]
-requires-python = ">=3.9"
+requires-python = ">=3.10"
 dependencies = [
     "SQLAlchemy>=1.4.0",
     "Mako",
@@ -111,7 +110,7 @@ version = {attr = "alembic.__version__"}
 
 [tool.black]
 line-length = 79
-target-version = ['py39']
+target-version = ['py310']
 
 [tool.pytest.ini_options]
 addopts = "--tb native -v -r sfxX -p no:warnings -p no:logging --maxfail=100"