- name: Install dependencies
run: |
python -m pip install --upgrade pip
- pip install --upgrade tox setuptools
+ pip install --upgrade nox setuptools
pip list
- name: Run tests
- run: tox -e github-${{ matrix.build-type }} -- ${{ matrix.pytest-args }}
+ run: nox -v -s github-${{ matrix.build-type }} -- ${{ matrix.pytest-args }}
- run-tox:
- name: ${{ matrix.tox-env }}-${{ matrix.python-version }}
+ run-nox:
+ name: ${{ matrix.nox-env }}-${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
- "ubuntu-22.04"
python-version:
- "3.13"
- tox-env:
+ nox-env:
- mypy
- - lint
- pep484
+ - pep8
fail-fast: false
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- pip install --upgrade tox setuptools
+ pip install --upgrade nox setuptools
pip list
- - name: Run tox
- run: tox -e ${{ matrix.tox-env }} ${{ matrix.pytest-args }}
+ - name: Run nox
+ run: nox -v -s ${{ matrix.nox-env }} -- ${{ matrix.pytest-args }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- pip install --upgrade tox setuptools
+ pip install --upgrade nox setuptools
pip list
- name: Run tests
- run: tox -e github-${{ matrix.build-type }} -- ${{ matrix.pytest-args }}
- env:
- # under free threading, make sure to disable GIL
- PYTHON_GIL: ${{ contains(matrix.python-version, 't') && '0' || '' }}
+ run: nox -v -s github-${{ matrix.build-type }} -- ${{ matrix.pytest-args }}
continue-on-error: ${{ matrix.python-version == 'pypy-3.10' }}
- run-tox:
- name: ${{ matrix.tox-env }}-${{ matrix.python-version }}
+ run-nox:
+ name: ${{ matrix.nox-env }}-${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
# run this job using this matrix, excluding some combinations below.
os:
- "ubuntu-22.04"
python-version:
- - "3.12"
- "3.13"
- tox-env:
+ nox-env:
- mypy
- pep484
-
- include:
- # run lint only on 3.13
- - tox-env: lint
- python-version: "3.13"
- os: "ubuntu-22.04"
+ - pep8
fail-fast: false
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- pip install --upgrade tox setuptools
+ pip install --upgrade nox setuptools
pip list
- - name: Run tox
- run: tox -e ${{ matrix.tox-env }} ${{ matrix.pytest-args }}
+ - name: Run nox
+ run: nox -v -e ${{ matrix.nox-env }} ${{ matrix.pytest-args }}
*.orig
*,cover
/.tox
+/.nox
/venv/
.venv
*.egg-info
Basic Test Running
==================
-Tox is used to run the test suite fully. For basic test runs against
+Nox is used to run the test suite fully. For basic test runs against
a single Python interpreter::
+ nox
+
+The previous runner, tox, still retains functionality in the near term however
+will eventually be removed::
+
+ # still works but deprecated
tox
-Advanced Tox Options
+The newer nox version retains most of the same kinds of functionality as the
+tox version, including a custom tagging utility that allows the nox runner
+to accept similar "tag" style arguments as were used by the tox runner.
+
+Advanced Nox Options
====================
-For more elaborate CI-style test running, the tox script provided will
+For more elaborate CI-style test running, the nox script provided will
run against various Python / database targets. For a basic run against
-Python 3.11 using an in-memory SQLite database::
+Python 3.13 using an in-memory SQLite database::
- tox -e py311-sqlite
+ nox -t py313-sqlite
-The tox runner contains a series of target combinations that can run
-against various combinations of databases. The test suite can be
-run against SQLite with "backend" tests also running against a PostgreSQL
-database::
+The nox runner contains a series of target combinations that can run
+against each database backend. Unlike the previous tox runner, targets
+that refer to multiple database backends at once are no longer
+supported at the nox level, in favor of running against multiple tags
+instead. So for example to run tests for sqlite and postgresql, while
+reducing how many tests run for postgresql to just those that are sensitive
+to the database backend::
- tox -e py311-sqlite-postgresql
+ nox -t py313-sqlite py313-postgresql-backendonly
-Or to run just "backend" tests against a MySQL database::
+Where above, the full suite will run against SQLite under Python 3.13, then
+the "backend only" version of the suite will for the PostgreSQL database.
- tox -e py311-mysql-backendonly
+The nox runner, like the tox runner before it, has options for running the
+tests with or without the Cython extensions built, with or without greenlet
+installed, as well as tags that select or deselect various memory/threading/
+performance intensive tests; the rules for how these environments are selected
+should be much more straightforward to understand with nox's imperative
+configuration style. For advanced use of nox it's worth it
+to poke around ``noxfile.py`` to get a general sense of what varieties
+of tests it can run.
Running against backends other than SQLite requires that a database of that
vendor be available at a specific URL. See "Setting Up Databases" below
The pytest Engine
=================
-The tox runner is using pytest to invoke the test suite. Within the realm of
+The nox runner uses pytest to invoke the test suite. Within the realm of
pytest, SQLAlchemy itself is adding a large series of option and
customizations to the pytest runner using plugin points, to allow for
SQLAlchemy's multiple database support, database setup/teardown and
pytest --db my_postgresql
We can also override the existing names in our ``test.cfg`` file, so that we can run
-with the tox runner also::
+with the nox/tox runners also::
# test.cfg file
[db]
postgresql=postgresql+psycopg2://username:pass@hostname/dbname
-Now when we run ``tox -e py311-postgresql``, it will use our custom URL instead
+Now when we run ``nox -t py313-postgresql``, it will use our custom URL instead
of the fixed one in setup.cfg.
Database Configuration
--- /dev/null
+.. change::
+ :tags: change, tests
+
+ The top-level test runner has been changed to use ``nox``, adding a
+ ``noxfile.py`` as well as some included modules. The ``tox.ini`` file
+ remains in place so that ``tox`` runs will continue to function in the near
+ term, however it will be eventually removed and improvements and
+ maintenance going forward will be only towards ``noxfile.py``.
+
+
--- /dev/null
+"""Nox configuration for SQLAlchemy."""
+
+from __future__ import annotations
+
+import os
+import sys
+from typing import Dict
+from typing import List
+from typing import Set
+
+import nox
+
+if True:
+ sys.path.insert(0, ".")
+ from tools.toxnox import extract_opts
+ from tools.toxnox import tox_parameters
+
+
+PYTHON_VERSIONS = ["3.10", "3.11", "3.12", "3.13", "3.13t", "3.14", "3.14t"]
+DATABASES = ["sqlite", "sqlite_file", "postgresql", "mysql", "oracle", "mssql"]
+CEXT = ["_auto", "cext", "nocext"]
+GREENLET = ["_greenlet", "nogreenlet"]
+BACKENDONLY = ["_all", "backendonly", "memusage"]
+
+# table of ``--dbdriver`` names to use on the pytest command line, which
+# match to dialect names
+DB_CLI_NAMES = {
+ "sqlite": {
+ "nogreenlet": {"sqlite", "pysqlite_numeric"},
+ "greenlet": {"aiosqlite"},
+ },
+ "sqlite_file": {
+ "nogreenlet": {"sqlite"},
+ "greenlet": {"aiosqlite"},
+ },
+ "postgresql": {
+ "nogreenlet": {"psycopg2", "pg8000", "psycopg"},
+ "greenlet": {"asyncpg", "psycopg_async"},
+ },
+ "mysql": {
+ "nogreenlet": {"mysqldb", "pymysql", "mariadbconnector"},
+ "greenlet": {"asyncmy", "aiomysql"},
+ },
+ "oracle": {
+ "nogreenlet": {"cx_oracle", "oracledb"},
+ "greenlet": {"oracledb_async"},
+ },
+ "mssql": {"nogreenlet": {"pyodbc", "pymssql"}, "greenlet": {"aioodbc"}},
+}
+
+
+def _setup_for_driver(
+ session: nox.Session,
+ cmd: List[str],
+ basename: str,
+ greenlet: bool = False,
+) -> None:
+
+ # install driver deps listed out in pyproject.toml
+ nogreenlet_deps = f"tests-{basename.replace("_", "-")}"
+ greenlet_deps = f"tests-{basename.replace("_", "-")}-asyncio"
+
+ deps = nox.project.dependency_groups(
+ pyproject,
+ (greenlet_deps if greenlet else nogreenlet_deps),
+ )
+ if deps:
+ session.install(*deps)
+
+ # set up top level ``--db`` sent to pytest command line, which looks
+ # up a base URL in the [db] section of setup.cfg. Environment variable
+ # substitution used by CI is also available.
+
+ # e.g. TOX_POSTGRESQL, TOX_MYSQL, etc.
+ dburl_env = f"TOX_{basename.upper()}"
+ # e.g. --db postgresql, --db mysql, etc.
+ default_dburl = f"--db {basename}"
+ cmd.extend(os.environ.get(dburl_env, default_dburl).split())
+
+ # set up extra drivers using --dbdriver. this first looks in
+ # an environment variable before making use of the DB_CLI_NAMES
+ # lookup table
+
+ # e.g. EXTRA_PG_DRIVERS, EXTRA_MYSQL_DRIVERS, etc.
+ if basename == "postgresql":
+ extra_driver_env = "EXTRA_PG_DRIVERS"
+ else:
+ extra_driver_env = f"EXTRA_{basename.upper()}_DRIVERS"
+ env_dbdrivers = os.environ.get(extra_driver_env, None)
+ if env_dbdrivers:
+ cmd.extend(env_dbdrivers.split())
+ return
+
+ # use fixed names in DB_CLI_NAMES
+ extra_drivers: Dict[str, Set[str]] = DB_CLI_NAMES[basename]
+ dbdrivers = extra_drivers["nogreenlet"]
+ if greenlet:
+ dbdrivers.update(extra_drivers["greenlet"])
+
+ for dbdriver in dbdrivers:
+ cmd.extend(["--dbdriver", dbdriver])
+
+
+pyproject = nox.project.load_toml("pyproject.toml")
+
+nox.options.sessions = ["tests"]
+nox.options.tags = ["py"]
+
+
+@nox.session()
+@tox_parameters(
+ ["python", "database", "cext", "greenlet", "backendonly"],
+ [
+ PYTHON_VERSIONS,
+ DATABASES,
+ CEXT,
+ GREENLET,
+ BACKENDONLY,
+ ],
+)
+def tests(
+ session: nox.Session,
+ database: str,
+ greenlet: str,
+ backendonly: str,
+ cext: str,
+) -> None:
+ """run the main test suite"""
+
+ _tests(
+ session,
+ database,
+ greenlet=greenlet == "_greenlet",
+ backendonly=backendonly == "backendonly",
+ platform_intensive=backendonly == "memusage",
+ cext=cext,
+ )
+
+
+@nox.session(name="coverage")
+@tox_parameters(
+ ["database", "cext", "backendonly"],
+ [DATABASES, CEXT, ["_all", "backendonly"]],
+ base_tag="coverage",
+)
+def coverage(
+ session: nox.Session, database: str, cext: str, backendonly: str
+) -> None:
+ """Run tests with coverage."""
+
+ _tests(
+ session,
+ database,
+ cext,
+ timing_intensive=False,
+ backendonly=backendonly == "backendonly",
+ coverage=True,
+ )
+
+
+@nox.session(name="github-cext-greenlet")
+def github_cext_greenlet(session: nox.Session) -> None:
+ """run tests for github actions"""
+
+ _tests(session, "sqlite", "cext", greenlet=True, timing_intensive=False)
+
+
+@nox.session(name="github-cext")
+def github_cext(session: nox.Session) -> None:
+ """run tests for github actions"""
+
+ _tests(session, "sqlite", "cext", greenlet=False, timing_intensive=False)
+
+
+@nox.session(name="github-nocext")
+def github_nocext(session: nox.Session) -> None:
+ """run tests for github actions"""
+
+ _tests(session, "sqlite", "cext", greenlet=False)
+
+
+def _tests(
+ session: nox.Session,
+ database: str,
+ cext: str = "_auto",
+ greenlet: bool = True,
+ backendonly: bool = False,
+ platform_intensive: bool = False,
+ timing_intensive: bool = True,
+ coverage: bool = False,
+ mypy: bool = False,
+) -> None:
+ # PYTHONNOUSERSITE - this *MUST* be set so that the ./lib/ import
+ # set up explicitly in test/conftest.py is *disabled*, so that
+ # when SQLAlchemy is built into the .nox area, we use that and not the
+ # local checkout, at least when usedevelop=False
+ session.env["PYTHONNOUSERSITE"] = "1"
+
+ freethreaded = isinstance(session.python, str) and session.python.endswith(
+ "t"
+ )
+
+ if freethreaded:
+ session.env["PYTHON_GIL"] = "0"
+
+ # greenlet frequently crashes with freethreading, so omit
+ # for the near future
+ greenlet = False
+
+ session.env["SQLALCHEMY_WARN_20"] = "1"
+
+ if cext == "cext":
+ session.env["REQUIRE_SQLALCHEMY_CEXT"] = "1"
+ elif cext == "nocext":
+ session.env["DISABLE_SQLALCHEMY_CEXT"] = "1"
+
+ includes_excludes: dict[str, list[str]] = {"k": [], "m": []}
+
+ if coverage:
+ timing_intensive = False
+
+ if platform_intensive:
+ # platform_intensive refers to test/aaa_profiling/test_memusage.py.
+ # it's only run exclusively of all other tests. does not include
+ # greenlet related tests
+ greenlet = False
+ # with "-m memory_intensive", only that suite will run, all
+ # other tests will be deselected by pytest
+ includes_excludes["m"].append("memory_intensive")
+ elif backendonly:
+ # with "-m backendonly", only tests with the backend pytest mark
+ # (or pytestplugin equivalent, like __backend__) will be selected
+ # by pytest
+ includes_excludes["m"].append("backend")
+ else:
+ includes_excludes["m"].append("not memory_intensive")
+
+ # the mypy suite is also run exclusively from the test_mypy
+ # session
+ includes_excludes["m"].append("not mypy")
+
+ if not timing_intensive:
+ includes_excludes["m"].append("not timing_intensive")
+
+ cmd = ["python", "-m", "pytest"]
+
+ if coverage:
+ assert not platform_intensive
+ cmd.extend(
+ [
+ "--cov=sqlalchemy",
+ "--cov-append",
+ "--cov-report",
+ "term",
+ "--cov-report",
+ "xml",
+ ],
+ )
+ includes_excludes["k"].append("not aaa_profiling")
+
+ cmd.extend(os.environ.get("TOX_WORKERS", "-n4").split())
+
+ if coverage:
+ session.install("-e", ".")
+ session.install(*nox.project.dependency_groups(pyproject, "coverage"))
+ else:
+ session.install(".")
+
+ session.install(*nox.project.dependency_groups(pyproject, "tests"))
+
+ if greenlet:
+ session.install(
+ *nox.project.dependency_groups(pyproject, "tests_greenlet")
+ )
+ else:
+ # note: if on SQLAlchemy 2.0, for "nogreenlet" need to do an explicit
+ # uninstall of greenlet since it's included in sqlalchemy dependencies
+ # in 2.1 it's an optional dependency
+ session.run("pip", "uninstall", "-y", "greenlet")
+
+ _setup_for_driver(session, cmd, database, greenlet=greenlet)
+
+ for letter, collection in includes_excludes.items():
+ if collection:
+ cmd.extend([f"-{letter}", " and ".join(collection)])
+
+ posargs, opts = extract_opts(session.posargs, "generate-junit", "dry-run")
+
+ if opts.generate_junit:
+ # produce individual junit files that are per-database
+ junitfile = f"junit-{database}.xml"
+ cmd.extend(["--junitxml", junitfile])
+
+ cmd.extend(posargs)
+
+ if opts.dry_run:
+ print(f"DRY RUN: command is: \n{' '.join(cmd)}")
+ return
+
+ try:
+ session.run(*cmd)
+ finally:
+ # Run cleanup for oracle/mssql
+ if database in ["oracle", "mssql"]:
+ session.run("python", "reap_dbs.py", "db_idents.txt")
+
+
+@nox.session(name="pep484")
+def test_pep484(session: nox.Session) -> None:
+ """Run mypy type checking."""
+
+ session.install(*nox.project.dependency_groups(pyproject, "mypy"))
+
+ session.install("-e", ".")
+
+ session.run(
+ "mypy",
+ "noxfile.py",
+ "./lib/sqlalchemy",
+ )
+
+
+@nox.session(name="mypy")
+def test_mypy(session: nox.Session) -> None:
+ """run the typing integration test suite"""
+
+ session.install(*nox.project.dependency_groups(pyproject, "mypy"))
+
+ session.install("-e", ".")
+
+ posargs, opts = extract_opts(session.posargs, "generate-junit")
+
+ cmd = ["pytest", "-m", "mypy"]
+ if opts.generate_junit:
+ # produce individual junit files that are per-database
+ junitfile = "junit-mypy.xml"
+ cmd.extend(["--junitxml", junitfile])
+
+ session.run(*cmd, *posargs)
+
+
+@nox.session(name="pep8")
+def test_pep8(session: nox.Session) -> None:
+ """Run linting and formatting checks."""
+
+ session.install("-e", ".")
+
+ session.install(*nox.project.dependency_groups(pyproject, "lint"))
+
+ for cmd in [
+ "flake8 ./lib/ ./test/ ./examples/ noxfile.py "
+ "setup.py doc/build/conf.py",
+ "flake8 --extend-ignore='' ./lib/sqlalchemy/ext/asyncio "
+ "./lib/sqlalchemy/orm/scoping.py",
+ "black --check ./lib/ ./test/ ./examples/ setup.py doc/build/conf.py",
+ "slotscheck -m sqlalchemy",
+ "python ./tools/format_docs_code.py --check",
+ "python ./tools/generate_tuple_map_overloads.py --check",
+ "python ./tools/generate_proxy_methods.py --check",
+ "python ./tools/sync_test_files.py --check",
+ "python ./tools/generate_sql_functions.py --check",
+ "python ./tools/normalize_file_headers.py --check",
+ "python ./tools/cython_imports.py --check",
+ "python ./tools/walk_packages.py",
+ ]:
+
+ session.run(*cmd.split())
postgresql = ["psycopg2>=2.7"]
postgresql-pg8000 = ["pg8000>=1.29.3"]
postgresql-asyncpg = [
- "greenlet>=1", # same as ".[asyncio]" if this syntax were supported
+ "sqlalchemy[asyncio]",
"asyncpg",
]
postgresql-psycopg2binary = ["psycopg2-binary"]
pymysql = ["pymysql"]
cymysql = ["cymysql"]
aiomysql = [
- "greenlet>=1", # same as ".[asyncio]" if this syntax were supported
+ "sqlalchemy[asyncio]",
"aiomysql",
]
aioodbc = [
- "greenlet>=1", # same as ".[asyncio]" if this syntax were supported
+ "sqlalchemy[asyncio]",
"aioodbc",
]
asyncmy = [
- "greenlet>=1", # same as ".[asyncio]" if this syntax were supported
+ "sqlalchemy[asyncio]",
"asyncmy>=0.2.3,!=0.2.4,!=0.2.6",
]
aiosqlite = [
- "greenlet>=1", # same as ".[asyncio]" if this syntax were supported
+ "sqlalchemy[asyncio]",
"aiosqlite",
]
sqlcipher = ["sqlcipher3_binary"]
postgresql_psycopg = ["sqlalchemy[postgresql-psycopg]"]
postgresql_psycopgbinary = ["sqlalchemy[postgresql-psycopgbinary]"]
+[dependency-groups]
+tests = [
+ "pytest>=7.0.0,<8.4",
+ "pytest-xdist",
+]
+
+coverage = ["pytest-cov"]
+
+tests-greenlet = ["sqlalchemy[asyncio]"]
+
+tests-sqlite = []
+
+tests-sqlite-asyncio = [
+ {include-group = "tests-greenlet"},
+ {include-group = "tests-sqlite"},
+ "sqlalchemy[aiosqlite]"
+]
+
+tests-sqlite-file = [{include-group = "tests-sqlite"}]
+tests-sqlite-file-asyncio = [{include-group = "tests-sqlite-asyncio"}]
+
+tests-postgresql = [
+ "sqlalchemy[postgresql]",
+ "sqlalchemy[postgresql-psycopg]",
+ "sqlalchemy[postgresql-pg8000]",
+]
+
+tests-postgresql-asyncio = [
+ {include-group = "tests-greenlet"},
+ {include-group = "tests-postgresql"},
+ "sqlalchemy[postgresql-asyncpg]"
+]
+
+tests-mysql = [
+ "sqlalchemy[mysql]",
+ "sqlalchemy[pymysql]",
+ "sqlalchemy[mysql-connector]",
+
+ # originally to fix https://jira.mariadb.org/browse/CONPY-318,
+ # more recent versions still have attribute errors and other random
+ # problems
+ "mariadb>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10,<1.1.13",
+
+]
+tests-mysql-asyncio = [
+ {include-group = "tests-greenlet"},
+ {include-group = "tests-mysql"},
+ "sqlalchemy[aiomysql]",
+ "sqlalchemy[asyncmy]",
+]
+
+tests-oracle-asyncio = [
+ {include-group = "tests-greenlet"},
+ {include-group = "tests-oracle"},
+]
+tests-oracle = [
+ "sqlalchemy[oracle]",
+ "sqlalchemy[oracle-oracledb]",
+]
+
+tests-mssql = [
+ "sqlalchemy[mssql]",
+ "sqlalchemy[mssql-pymssql]",
+]
+
+tests-mssql-asyncio = [
+ {include-group = "tests-greenlet"},
+ {include-group = "tests-mssql"},
+ "aioodbc"
+]
+
+lint = [
+ {include-group = "tests-greenlet"},
+ "flake8>=7.2.0",
+ "flake8-import-order>=0.19.2",
+ "flake8-import-single==0.1.5",
+ "flake8-builtins",
+ "flake8-future-annotations>=0.0.5",
+ "flake8-docstrings",
+ "flake8-unused-arguments",
+ "flake8-rst-docstrings",
+ "pydocstyle<4.0.0",
+ "pygments",
+ "black==25.1.0",
+ "slotscheck>=0.17.0",
+ "zimports", # required by generate_tuple_map_overloads
+]
+
+mypy = [
+ {include-group = "tests-greenlet"},
+ "mypy>=1.16.0",
+ "nox", # because we check noxfile.py
+ "pytest>8,<8.4", # alembic/testing imports pytest
+ "types-greenlet",
+]
+
+
[tool.setuptools]
include-package-data = true
--- /dev/null
+"""Provides the tox_parameters() utility, which generates parameterized
+sections for nox tests, which include tags that indicate various combinations
+of those parameters in such a way that it's somewhat similar to how
+we were using the tox project; where individual dash-separated tags could
+be added to add more specificity to the suite configuation, or omitting them
+would fall back to defaults.
+
+
+"""
+
+from __future__ import annotations
+
+import collections
+import re
+import sys
+from typing import Any
+from typing import Callable
+from typing import Generator
+from typing import Sequence
+
+import nox
+
+OUR_PYTHON = f"{sys.version_info.major}.{sys.version_info.minor}"
+
+
+def tox_parameters(
+ names: Sequence[str],
+ token_lists: Sequence[Sequence[str]],
+ *,
+ base_tag: str | None = None,
+ filter_: Callable[..., bool] | None = None,
+ always_include_in_tag: Sequence[str] | None = None,
+) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
+ r"""Decorator to create a parameter/tagging structure for a nox session
+ function that acts to a large degree like tox's generative environments.
+
+ The output is a ``nox.parametrize()`` decorator that's built up from
+ individual ``nox.param()`` instances.
+
+ :param names: names of the parameters sent to the session function.
+ These names go straight to the first argument of ``nox.parametrize()``
+ and should all match argument names accepted by the decorated function
+ (except for ``python``, which is optional).
+ :param token_lists: a sequence of lists of values for each parameter. a
+ ``nox.param()`` will be created for the full product of these values,
+ minus those filtered out using the ``filter_`` callable. These tokens
+ are used to create the args, tags, and ids of each ``nox.param()``. The
+ list of tags will be generated out including all values for a parameter
+ joined by ``-``, as well as combinations that include a subset of those
+ values, where the omitted elements of the tag are implicitly considered to
+ match the "default" value, indicated by them being first in their
+ collection (with the exception of "python", where the current python in
+ use is the default). Additionally, values that start with an underscore
+ are omitted from all ids and tags. Values that refer to Python versions
+ wlil be expanded to the full Python executable name when passed as
+ arguments to the session function, which is currently a workaround to
+ allow free-threaded python interpreters to be located.
+ :param base_tag: optional tag that will be appended to all tags generated,
+ e.g. if the decorator yields tags like ``python314-x86-windows``, a
+ ``basetag`` value of ``all`` would yield the
+ tag as ``python314-x86-windows-all``.
+ :param filter\_: optional filtering function, must accept keyword arguments
+ matching the names in ``names``. Returns True or False indicating if
+ a certain tag combination should be included.
+ :param always_include_in_tag: list of names from ``names`` that indicate
+ parameters that should always be part of all tags, and not be omitted
+ as a "default"
+
+
+ """
+
+ PY_RE = re.compile(r"(?:python)?([234]\.\d+(t?))")
+
+ def _is_py_version(token: str) -> bool:
+ return bool(PY_RE.match(token))
+
+ def _expand_python_version(token: str) -> str:
+ """expand pyx.y(t) tags into executable names.
+
+ Works around nox issue fixed at
+ https://github.com/wntrblm/nox/pull/999 by providing full executable
+ name
+
+ """
+ if sys.platform == "win32":
+ return token
+
+ m = PY_RE.match(token)
+
+ # do this matching minimally so that it only happens for the
+ # free-threaded versions. on windows, the "pythonx.y" syntax doesn't
+ # work due to the use of the "py" tool
+ if m and m.group(2) == "t":
+ return f"python{m.group(1)}"
+ else:
+ return token
+
+ def _python_to_tag(token: str) -> str:
+ m = PY_RE.match(token)
+ if m:
+ return f"py{m.group(1).replace('.', '')}"
+ else:
+ return token
+
+ if always_include_in_tag:
+ name_to_list = dict(zip(names, token_lists))
+ must_be_present = [
+ name_to_list[name] for name in always_include_in_tag
+ ]
+ else:
+ must_be_present = None
+
+ def _recur_param(
+ prevtokens: list[str],
+ prevtags: list[str],
+ token_lists: Sequence[Sequence[str]],
+ ) -> Generator[tuple[list[str], list[str], str], None, None]:
+
+ if not token_lists:
+ return
+
+ tokens = token_lists[0]
+ remainder = token_lists[1:]
+
+ for i, token in enumerate(tokens):
+
+ if _is_py_version(token):
+ is_our_python = token == OUR_PYTHON
+ tokentag = _python_to_tag(token)
+ is_default_token = is_our_python
+ else:
+ is_our_python = False
+ tokentag = token
+ is_default_token = i == 0
+
+ if is_our_python:
+ our_python_tags = ["py"]
+ else:
+ our_python_tags = []
+
+ if not tokentag.startswith("_"):
+ tags = (
+ prevtags
+ + [tokentag]
+ + [tag + "-" + tokentag for tag in prevtags]
+ + our_python_tags
+ )
+ else:
+ tags = prevtags + our_python_tags
+
+ if remainder:
+ for args, newtags, ids in _recur_param(
+ prevtokens + [token], tags, remainder
+ ):
+ if not is_default_token:
+ newtags = [
+ t
+ for t in newtags
+ if tokentag in t or t in our_python_tags
+ ]
+
+ yield args, newtags, ids
+ else:
+ if not is_default_token:
+ newtags = [
+ t
+ for t in tags
+ if tokentag in t or t in our_python_tags
+ ]
+ else:
+ newtags = tags
+
+ if base_tag:
+ newtags = [t + f"-{base_tag}" for t in newtags]
+ if must_be_present:
+ for t in list(newtags):
+ for required_tokens in must_be_present:
+ if not any(r in t for r in required_tokens):
+ newtags.remove(t)
+ break
+
+ yield prevtokens + [token], newtags, "-".join(
+ _python_to_tag(t)
+ for t in prevtokens + [token]
+ if not t.startswith("_")
+ )
+
+ params = [
+ nox.param(
+ *[_expand_python_version(a) for a in args], tags=tags, id=ids
+ )
+ for args, tags, ids in _recur_param([], [], token_lists)
+ if filter_ is None or filter_(**dict(zip(names, args)))
+ ]
+
+ # for p in params:
+ # print(f"PARAM {'-'.join(p.args)} TAGS {p.tags}")
+
+ return nox.parametrize(names, params)
+
+
+def extract_opts(posargs: list[str], *args: str) -> tuple[list[str], Any]:
+ """Pop individual flag options from session.posargs.
+
+ Returns a named tuple with the individual flag options indicated,
+ as well the new posargs with those flags removed from the string list
+ so that the posargs can be forwarded onto pytest.
+
+ Basically if nox had an option for additional environmental flags that
+ didn't require putting them after ``--``, we wouldn't need this, but this
+ is probably more flexible.
+
+ """
+ underscore_args = [arg.replace("-", "_") for arg in args]
+ return_tuple = collections.namedtuple("options", underscore_args) # type: ignore # noqa: E501
+
+ look_for_args = {f"--{arg}": idx for idx, arg in enumerate(args)}
+ return_args = [False for arg in args]
+
+ def extract(arg: str) -> bool:
+ if arg in look_for_args:
+ return_args[look_for_args[arg]] = True
+ return True
+ else:
+ return False
+
+ return [arg for arg in posargs if not extract(arg)], return_tuple(
+ *return_args
+ )
--- /dev/null
+def warn_tox():
+ print(
+ "\n"
+ + "=" * 80
+ + "\n\033[1;31m ⚠️ NOTE: TOX IS DEPRECATED IN THIS PROJECT! ⚠️"
+ "\033[0m\n\033[1;33m "
+ "Please use nox instead for running tests.\033[0m\n" + "=" * 80 + "\n"
+ )
+
+
+if __name__ == "__main__":
+ warn_tox()
# PYTHONPATH - erased so that we use the build that's present
# in .tox as the SQLAlchemy library to be imported
#
-# PYTHONUSERSITE - this *MUST* be set so that the ./lib/ import
+# PYTHONNOUSERSITE - this *MUST* be set so that the ./lib/ import
# set up explicitly in test/conftest.py is *disabled*, again so that
# when SQLAlchemy is built into the .tox area, we use that and not the
# local checkout, at least when usedevelop=False
nogreenlet: pip uninstall -y greenlet
{env:BASECOMMAND} {env:WORKERS} {env:SQLITE:} {env:EXTRA_SQLITE_DRIVERS:} {env:POSTGRESQL:} {env:EXTRA_PG_DRIVERS:} {env:MYSQL:} {env:EXTRA_MYSQL_DRIVERS:} {env:ORACLE:} {env:EXTRA_ORACLE_DRIVERS:} {env:MSSQL:} {env:EXTRA_MSSQL_DRIVERS:} {env:IDENTS:} {env:PYTEST_EXCLUDES:} {env:COVERAGE:} {posargs}
oracle,mssql,sqlite_file: python reap_dbs.py db_idents.txt
+ python tools/warn_tox.py
[testenv:pep484]
types-greenlet
commands =
mypy {env:MYPY_COLOR} ./lib/sqlalchemy
- # pyright changes too often with not-exactly-correct errors
- # suddently appearing for it to be stable enough for CI
- # pyright
+ python tools/warn_tox.py
extras =
{[greenletextras]extras}
commands =
pytest {env:PYTEST_COLOR} -m mypy {posargs}
+ python tools/warn_tox.py
[testenv:mypy-cov]
commands =
pytest {env:PYTEST_COLOR} -m mypy {env:COVERAGE} {posargs}
+ python tools/warn_tox.py
setenv=
COVERAGE={[testenv]cov_args}