From 941d660d40c876fdf8428a7f8d2e3d4ab55e68bd Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 28 Sep 2025 23:44:41 -0400 Subject: [PATCH] add nox support (but dont switch out fully) This backports the nox change from main/2.1 so that we have full nox support available for the 2.0 series, however does not modify any existing docs or test systems. A noxfile.py has been added to allow testing with nox. This is a direct port of 2.1's move to nox, however leaves the tox.ini file in place and retains all test documentation in terms of tox. Version 2.1 will move to nox fully, including deprecation warnings for tox and new testing documentation. Change-Id: I66639991e1dc3db582e2ff13f9348a7d6241916e (cherry picked from commit df899e94cf7ba18f4e7151ef173393be78c56c3f) (also cherry-picked from 1577c1d15b) --- .github/workflows/run-on-pr.yaml | 20 +- .github/workflows/run-test.yaml | 27 +- .gitignore | 1 + MANIFEST.in | 2 +- doc/build/changelog/unreleased_20/use_nox.rst | 8 + noxfile.py | 385 ++++++++++++++++++ pyproject.toml | 97 +++++ tools/toxnox.py | 229 +++++++++++ tools/warn_tox.py | 12 + tox.ini | 5 +- 10 files changed, 753 insertions(+), 33 deletions(-) create mode 100644 doc/build/changelog/unreleased_20/use_nox.rst create mode 100644 noxfile.py create mode 100644 tools/toxnox.py create mode 100644 tools/warn_tox.py diff --git a/.github/workflows/run-on-pr.yaml b/.github/workflows/run-on-pr.yaml index 3e2f1b39f6..54ede65ced 100644 --- a/.github/workflows/run-on-pr.yaml +++ b/.github/workflows/run-on-pr.yaml @@ -49,25 +49,25 @@ jobs: - 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: os: - "ubuntu-22.04" python-version: - - "3.12" - tox-env: + - "3.13" + nox-env: - mypy - - lint - pep484 + - pep8 fail-fast: false @@ -84,8 +84,8 @@ jobs: - 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 }} diff --git a/.github/workflows/run-test.yaml b/.github/workflows/run-test.yaml index 15a7f9a510..8096d82348 100644 --- a/.github/workflows/run-test.yaml +++ b/.github/workflows/run-test.yaml @@ -143,18 +143,15 @@ jobs: - 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. @@ -162,17 +159,11 @@ jobs: os: - "ubuntu-22.04" python-version: - - "3.12" - "3.13" - tox-env: + nox-env: - mypy - pep484 - - include: - # run lint only on 3.12 - - tox-env: lint - python-version: "3.12" - os: "ubuntu-22.04" + - pep8 fail-fast: false @@ -190,8 +181,8 @@ jobs: - 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 }} diff --git a/.gitignore b/.gitignore index d2ee9a2f4a..4b76721abf 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ *.orig *,cover /.tox +/.nox /venv/ .venv *.egg-info diff --git a/MANIFEST.in b/MANIFEST.in index 7a272fe6b4..a5ee9fcedb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,5 +15,5 @@ exclude lib/sqlalchemy/cyextension/*.so # don't come in if --with-cextensions isn't specified. recursive-include lib *.pyx *.pxd *.txt *.typed -include README* AUTHORS LICENSE CHANGES* tox.ini +include README* AUTHORS LICENSE CHANGES* tox.ini noxfile.py prune doc/build/output diff --git a/doc/build/changelog/unreleased_20/use_nox.rst b/doc/build/changelog/unreleased_20/use_nox.rst new file mode 100644 index 0000000000..ee57aa679f --- /dev/null +++ b/doc/build/changelog/unreleased_20/use_nox.rst @@ -0,0 +1,8 @@ +.. change:: + :tags: change, tests + + A noxfile.py has been added to allow testing with nox. This is a direct + port of 2.1's move to nox, however leaves the tox.ini file in place and + retains all test documentation in terms of tox. Version 2.1 will move to + nox fully, including deprecation warnings for tox and new testing + documentation. diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000000..16ba3023a5 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,385 @@ +"""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.7", + "3.8", + "3.9", + "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]) + + if database in ["oracle", "mssql", "sqlite_file"]: + cmd.extend(["--write-idents", "db_idents.txt"]) + + 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", "sqlite_file"]: + if os.path.exists("db_idents.txt"): + session.run("python", "reap_dbs.py", "db_idents.txt") + os.unlink("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 " + "noxfile.py", + # test with cython and without cython exts running + "slotscheck -m sqlalchemy", + "env DISABLE_SQLALCHEMY_CEXT_RUNTIME=1 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/walk_packages.py", + ]: + + session.run(*cmd.split(), external=True) diff --git a/pyproject.toml b/pyproject.toml index 570dfc4031..bc511e0250 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,103 @@ requires = [ "cython>=0.29.24; platform_python_implementation == 'CPython'", # Skip cython when using pypy ] +[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.black] line-length = 79 target-version = ['py37'] diff --git a/tools/toxnox.py b/tools/toxnox.py new file mode 100644 index 0000000000..897abfc774 --- /dev/null +++ b/tools/toxnox.py @@ -0,0 +1,229 @@ +"""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 + ) diff --git a/tools/warn_tox.py b/tools/warn_tox.py new file mode 100644 index 0000000000..a4530b8928 --- /dev/null +++ b/tools/warn_tox.py @@ -0,0 +1,12 @@ +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() diff --git a/tox.ini b/tox.ini index bbc9536df6..2359df1d9d 100644 --- a/tox.ini +++ b/tox.ini @@ -95,7 +95,7 @@ allowlist_externals=sh # 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 @@ -209,9 +209,6 @@ deps= 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 extras = {[greenletextras]extras} -- 2.47.3