From 470a7da04a440a328c70b288f2945aa12e71d066 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 11 Oct 2025 10:28:35 -0400 Subject: [PATCH] drop py39 support Change-Id: I89a636ac3e687240ffb464acb8ade5055bba408b --- alembic/__init__.py | 2 +- alembic/script/base.py | 8 +--- alembic/script/write_hooks.py | 16 +++---- alembic/templates/async/alembic.ini.mako | 4 +- alembic/templates/generic/alembic.ini.mako | 4 +- alembic/templates/multidb/alembic.ini.mako | 4 +- .../templates/pyproject/pyproject.toml.mako | 4 +- .../pyproject_async/pyproject.toml.mako | 4 +- alembic/testing/fixtures.py | 27 ++++++++++-- alembic/util/compat.py | 44 ++++++------------- alembic/util/pyfiles.py | 6 +-- docs/build/changelog.rst | 2 +- docs/build/unreleased/remove_py39.rst | 4 ++ noxfile.py | 11 +++-- pyproject.toml | 5 +-- 15 files changed, 75 insertions(+), 70 deletions(-) create mode 100644 docs/build/unreleased/remove_py39.rst diff --git a/alembic/__init__.py b/alembic/__init__.py index c3ae61d2..7685cfa6 100644 --- a/alembic/__init__.py +++ b/alembic/__init__.py @@ -1,4 +1,4 @@ from . import context from . import op -__version__ = "1.16.6" +__version__ = "1.17.0" diff --git a/alembic/script/base.py b/alembic/script/base.py index 94292316..6b9692c6 100644 --- a/alembic/script/base.py +++ b/alembic/script/base.py @@ -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] diff --git a/alembic/script/write_hooks.py b/alembic/script/write_hooks.py index f40bb35f..6b8161db 100644 --- a/alembic/script/write_hooks.py +++ b/alembic/script/write_hooks.py @@ -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 diff --git a/alembic/templates/async/alembic.ini.mako b/alembic/templates/async/alembic.ini.mako index 67acc6d0..62617c4c 100644 --- a/alembic/templates/async/alembic.ini.mako +++ b/alembic/templates/async/alembic.ini.mako @@ -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 = diff --git a/alembic/templates/generic/alembic.ini.mako b/alembic/templates/generic/alembic.ini.mako index bb93d0e3..eb8c063f 100644 --- a/alembic/templates/generic/alembic.ini.mako +++ b/alembic/templates/generic/alembic.ini.mako @@ -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 = diff --git a/alembic/templates/multidb/alembic.ini.mako b/alembic/templates/multidb/alembic.ini.mako index a6629839..f4b65e62 100644 --- a/alembic/templates/multidb/alembic.ini.mako +++ b/alembic/templates/multidb/alembic.ini.mako @@ -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 = diff --git a/alembic/templates/pyproject/pyproject.toml.mako b/alembic/templates/pyproject/pyproject.toml.mako index e68cef33..224faec1 100644 --- a/alembic/templates/pyproject/pyproject.toml.mako +++ b/alembic/templates/pyproject/pyproject.toml.mako @@ -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 = diff --git a/alembic/templates/pyproject_async/pyproject.toml.mako b/alembic/templates/pyproject_async/pyproject.toml.mako index e68cef33..224faec1 100644 --- a/alembic/templates/pyproject_async/pyproject.toml.mako +++ b/alembic/templates/pyproject_async/pyproject.toml.mako @@ -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 = diff --git a/alembic/testing/fixtures.py b/alembic/testing/fixtures.py index 61bcd7e9..6d801fcb 100644 --- a/alembic/testing/fixtures.py +++ b/alembic/testing/fixtures.py @@ -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 diff --git a/alembic/util/compat.py b/alembic/util/compat.py index 131f16a0..527ffdc9 100644 --- a/alembic/util/compat.py +++ b/alembic/util/compat.py @@ -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") diff --git a/alembic/util/pyfiles.py b/alembic/util/pyfiles.py index 6b75d577..135a42dc 100644 --- a/alembic/util/pyfiles.py +++ b/alembic/util/pyfiles.py @@ -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) diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index 2ed6d226..0bd8d88b 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -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 index 00000000..4c300bb9 --- /dev/null +++ b/docs/build/unreleased/remove_py39.rst @@ -0,0 +1,4 @@ +.. change:: + :tags: change, general + + The minimum Python version is now 3.10, as Python 3.9 is EOL. diff --git a/noxfile.py b/noxfile.py index 7ce6a609..3ffc096f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -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: diff --git a/pyproject.toml b/pyproject.toml index 3b9bed6c..48c42569 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" -- 2.47.3