Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-tox for openSUSE:Factory checked in at 2026-01-17 14:55:01 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-tox (Old) and /work/SRC/openSUSE:Factory/.python-tox.new.1928 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-tox" Sat Jan 17 14:55:01 2026 rev:61 rq:1327617 version:4.27.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-tox/python-tox.changes 2025-07-10 22:11:38.750460864 +0200 +++ /work/SRC/openSUSE:Factory/.python-tox.new.1928/python-tox.changes 2026-01-17 14:56:16.078981852 +0100 @@ -1,0 +2,20 @@ +Fri Jan 16 09:40:18 UTC 2026 - Dirk Müller <[email protected]> + +update to 4.27.0: + * Feat: include free_threaded flag in result-json + * Add security policy + * Fix dependency-group name normalization + * Log environment variables sorted by key while redacting + values of unsafe ones +- update to 4.26.0: + * Add a missing quote in a TOML example @ `config.rst` + * Add colour to GitHub Actions CI logs + * Fix using deprecated virtualenv option `--wheel` + * Fix custom HelpFormatter for Python 3.14 + * Drop support for EOL Python 3.8 + * Test with Python 3.14 + * Fix for tox4 regression issue with setenv file and + substitutions + * Feat: free-threaded python support + +------------------------------------------------------------------- Old: ---- tox-4.25.0.tar.gz New: ---- tox-4.27.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-tox.spec ++++++ --- /var/tmp/diff_new_pack.UnDqL1/_old 2026-01-17 14:56:16.675006771 +0100 +++ /var/tmp/diff_new_pack.UnDqL1/_new 2026-01-17 14:56:16.675006771 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-tox # -# Copyright (c) 2025 SUSE LLC +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -23,7 +23,7 @@ %bcond_with devpi_process %endif Name: python-tox -Version: 4.25.0 +Version: 4.27.0 Release: 0 Summary: Virtualenv-based automation of test activities License: MIT @@ -54,7 +54,7 @@ BuildRequires: %{python_module setuptools >= 41.0.1} BuildRequires: %{python_module setuptools_scm >= 2.0.0} BuildRequires: %{python_module time-machine >= 2.13} -BuildRequires: %{python_module virtualenv >= 20.29.1} +BuildRequires: %{python_module virtualenv >= 20.31} BuildRequires: %{python_module wheel >= 0.42} %if %{with devpi_process} BuildRequires: %{python_module devpi-process > 1} @@ -71,7 +71,7 @@ Requires: python-platformdirs >= 4.3.6 Requires: python-pluggy >= 1.5 Requires: python-pyproject-api >= 1.8 -Requires: python-virtualenv >= 20.29.1 +Requires: python-virtualenv >= 20.31 Requires(post): update-alternatives Requires(postun): update-alternatives # last detox version is 0.19 ++++++ tox-4.25.0.tar.gz -> tox-4.27.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/PKG-INFO new/tox-4.27.0/PKG-INFO --- old/tox-4.25.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: tox -Version: 4.25.0 +Version: 4.27.0 Summary: tox is a generic virtualenv management and test command line tool Project-URL: Documentation, https://tox.wiki Project-URL: Homepage, http://tox.readthedocs.org @@ -20,16 +20,16 @@ Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Utilities -Requires-Python: >=3.8 +Requires-Python: >=3.9 Requires-Dist: cachetools>=5.5.1 Requires-Dist: chardet>=5.2 Requires-Dist: colorama>=0.4.6 @@ -40,7 +40,7 @@ Requires-Dist: pyproject-api>=1.8 Requires-Dist: tomli>=2.2.1; python_version < '3.11' Requires-Dist: typing-extensions>=4.12.2; python_version < '3.11' -Requires-Dist: virtualenv>=20.29.1 +Requires-Dist: virtualenv>=20.31 Provides-Extra: test Requires-Dist: devpi-process>=1.0.2; extra == 'test' Requires-Dist: pytest-mock>=3.14; extra == 'test' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/pyproject.toml new/tox-4.27.0/pyproject.toml --- old/tox-4.25.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 @@ -26,7 +26,7 @@ authors = [ { name = "Bernát Gábor", email = "[email protected]" }, ] -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: tox", @@ -36,12 +36,12 @@ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Testing", "Topic :: Utilities", @@ -60,7 +60,7 @@ "pyproject-api>=1.8", "tomli>=2.2.1; python_version<'3.11'", "typing-extensions>=4.12.2; python_version<'3.11'", - "virtualenv>=20.29.1", + "virtualenv>=20.31", ] optional-dependencies.test = [ "devpi-process>=1.0.2", @@ -83,6 +83,7 @@ test = [ "build[virtualenv]>=1.2.2.post1", "covdefaults>=2.3", + "coverage>=7.9.1", "detect-test-pollution>=1.2", "devpi-process>=1.0.2", "diff-cover>=9.2", @@ -195,7 +196,7 @@ count = true [tool.pyproject-fmt] -max_supported_python = "3.13" +max_supported_python = "3.14" [tool.pytest.ini_options] testpaths = [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/src/tox/config/cli/parser.py new/tox-4.27.0/src/tox/config/cli/parser.py --- old/tox-4.25.0/src/tox/config/cli/parser.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/src/tox/config/cli/parser.py 2020-02-02 01:00:00.000000000 +0100 @@ -95,8 +95,8 @@ class HelpFormatter(ArgumentDefaultsHelpFormatter): """A help formatter that provides the default value and the source it comes from.""" - def __init__(self, prog: str) -> None: - super().__init__(prog, max_help_position=30, width=240) + def __init__(self, prog: str, **kwargs: Any) -> None: + super().__init__(prog, max_help_position=30, width=240, **kwargs) def _get_help_string(self, action: Action) -> str | None: text: str = super()._get_help_string(action) or "" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/src/tox/config/set_env.py new/tox-4.27.0/src/tox/config/set_env.py --- old/tox-4.25.0/src/tox/config/set_env.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/src/tox/config/set_env.py 2020-02-02 01:00:00.000000000 +0100 @@ -34,8 +34,8 @@ return for line in raw.splitlines(): # noqa: PLR1702 if line.strip(): - if line.startswith("file|"): # environment files to be handled later - self._env_files.append(line[len("file|") :]) + if self._is_file_line(line): + self._env_files.append(self._parse_file_line(line)) else: try: key, value = self._extract_key_value(line) @@ -52,12 +52,20 @@ else: self._raw[key] = value + @staticmethod + def _is_file_line(line: str) -> bool: + return line.startswith("file|") + + @staticmethod + def _parse_file_line(line: str) -> str: + return line[len("file|") :] + def use_replacer(self, value: Replacer, args: ConfigLoadArgs) -> None: self._replacer = value for filename in self._env_files: - self._read_env_file(filename, args) + self._raw.update(self._stream_env_file(filename, args)) - def _read_env_file(self, filename: str, args: ConfigLoadArgs) -> None: + def _stream_env_file(self, filename: str, args: ConfigLoadArgs) -> Iterator[tuple[str, str]]: # Our rules in the documentation, some upstream environment file rules (we follow mostly the docker one): # - https://www.npmjs.com/package/dotenv#rules # - https://docs.docker.com/compose/env-file/ @@ -70,8 +78,7 @@ env_line = env_line.strip() # noqa: PLW2901 if not env_line or env_line.startswith("#"): continue - key, value = self._extract_key_value(env_line) - self._raw[key] = value + yield self._extract_key_value(env_line) @staticmethod def _extract_key_value(line: str) -> tuple[str, str]: @@ -100,10 +107,18 @@ # start with the materialized ones, maybe we don't need to materialize the raw ones yield from self._materialized.keys() yield from list(self._raw.keys()) # iterating over this may trigger materialization and change the dict + args = ConfigLoadArgs([], self._name, self._env_name) while self._needs_replacement: line = self._needs_replacement.pop(0) - expanded_line = self._replacer(line, ConfigLoadArgs([], self._name, self._env_name)) - sub_raw = dict(self._extract_key_value(sub_line) for sub_line in expanded_line.splitlines() if sub_line) + expanded_line = self._replacer(line, args) + sub_raw: dict[str, str] = {} + for sub_line in filter(None, expanded_line.splitlines()): + if not self._is_file_line(sub_line): + sub_raw.__setitem__(*self._extract_key_value(sub_line)) + else: + for key, value in self._stream_env_file(self._parse_file_line(sub_line), args): + if key not in self._raw: + sub_raw[key] = value # noqa: PERF403 self._raw.update(sub_raw) self.changed = True # loading while iterating can cause these values to be missed yield from sub_raw.keys() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/src/tox/pytest.py new/tox-4.27.0/src/tox/pytest.py --- old/tox-4.25.0/src/tox/pytest.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/src/tox/pytest.py 2020-02-02 01:00:00.000000000 +0100 @@ -281,7 +281,8 @@ m.setenv("VIRTUALENV_SYMLINK_APP_DATA", "1") m.setenv("VIRTUALENV_SYMLINKS", "1") m.setenv("VIRTUALENV_PIP", "embed") - m.setenv("VIRTUALENV_WHEEL", "embed") + if sys.version_info[:2] < (3, 9): + m.setenv("VIRTUALENV_WHEEL", "embed") m.setenv("VIRTUALENV_SETUPTOOLS", "embed") try: tox_run(args) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/src/tox/session/env_select.py new/tox-4.27.0/src/tox/session/env_select.py --- old/tox-4.25.0/src/tox/session/env_select.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/src/tox/session/env_select.py 2020-02-02 01:00:00.000000000 +0100 @@ -136,7 +136,7 @@ package_skip: tuple[str, Skip] | None = None #: if set the creation of the packaging environment failed -_DYNAMIC_ENV_FACTORS = re.compile(r"(pypy|py|cython|)((\d(\.\d+(\.\d+)?)?)|\d+)?") +_DYNAMIC_ENV_FACTORS = re.compile(r"(pypy|py|cython|)(((\d(\.\d+(\.\d+)?)?)|\d+)t?)?") _PY_PRE_RELEASE_FACTOR = re.compile(r"alpha|beta|rc\.\d+") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/src/tox/tox_env/api.py new/tox-4.27.0/src/tox/tox_env/api.py --- old/tox-4.25.0/src/tox/tox_env/api.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/src/tox/tox_env/api.py 2020-02-02 01:00:00.000000000 +0100 @@ -31,6 +31,30 @@ from tox.tox_env.installer import Installer LOGGER = logging.getLogger(__name__) +# Based on original gitleaks rule named generic-api-key +# See: https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml#L587 +SECRET_KEYWORDS = [ + "access", + "api", + "auth", + "client", + "cred", + "key", + "passwd", + "password", + "private", + "pwd", + "secret", + "token", +] +SECRET_ENV_VAR_REGEX = re.compile(".*(" + "|".join(SECRET_KEYWORDS) + ").*", re.IGNORECASE) + + +def redact_value(name: str, value: str) -> str: + """Returns a redacted text if the key name looks like a secret.""" + if SECRET_ENV_VAR_REGEX.match(name): + return "*" * len(value) + return value class ToxEnvCreateArgs(NamedTuple): @@ -222,6 +246,7 @@ "FORCE_COLOR", # force color output "NO_COLOR", # disable color output "NETRC", # used by pip and netrc modules + "PYTHON_GIL", # allows controlling python gil ] if sys.stdout.isatty(): # if we're on a interactive shell pass on the TERM env.append("TERM") @@ -460,8 +485,11 @@ with log_file.open("wt", encoding="utf-8") as file: file.write(f"name: {env_name}\n") file.write(f"run_id: {request.run_id}\n") - for env_key, env_value in request.env.items(): - file.write(f"env {env_key}: {env_value}\n") + msg = "" + for env_key, env_value in sorted(request.env.items()): + redacted_value = redact_value(name=env_key, value=env_value) + msg += f"env {env_key}: {redacted_value}\n" + file.write(msg) for meta_key, meta_value in status.metadata.items(): file.write(f"metadata {meta_key}: {meta_value}\n") file.write(f"cwd: {request.cwd}\n") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/src/tox/tox_env/python/api.py new/tox-4.27.0/src/tox/tox_env/python/api.py --- old/tox-4.25.0/src/tox/tox_env/python/api.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/src/tox/tox_env/python/api.py 2020-02-02 01:00:00.000000000 +0100 @@ -5,9 +5,11 @@ import logging import re import sys +import sysconfig from abc import ABC, abstractmethod +from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, List, NamedTuple, cast +from typing import TYPE_CHECKING, Any, List, NamedTuple from virtualenv.discovery.py_spec import PythonSpec @@ -26,13 +28,15 @@ serial: int -class PythonInfo(NamedTuple): +@dataclass(frozen=True) +class PythonInfo: implementation: str version_info: VersionInfo version: str is_64: bool platform: str extra: dict[str, Any] + free_threaded: bool = False @property def version_no_dot(self) -> str: @@ -51,11 +55,14 @@ r""" ^(?!py$) # don't match 'py' as it doesn't provide any info (?P<impl>py|pypy|cpython|jython|graalpy|rustpython|ironpython) # the interpreter; most users will simply use 'py' - (?P<version>[2-9]\.?[0-9]?[0-9]?)?$ # the version; one of: MAJORMINOR, MAJOR.MINOR + (?: + (?P<version>[2-9]\.?[0-9]?[0-9]?) # the version; one of: MAJORMINOR, MAJOR.MINOR + (?P<threaded>t?) # version followed by t for free-threading + )?$ """, re.VERBOSE, ) -PY_FACTORS_RE_EXPLICIT_VERSION = re.compile(r"^((?P<impl>cpython|pypy)-)?(?P<version>[2-9]\.[0-9]+)$") +PY_FACTORS_RE_EXPLICIT_VERSION = re.compile(r"^((?P<impl>cpython|pypy)-)?(?P<version>[2-9]\.[0-9]+)(?P<threaded>t?)$") class Python(ToxEnv, ABC): @@ -100,6 +107,7 @@ ) self.conf.add_constant("py_dot_ver", "<python major>.<python minor>", value=self.py_dot_ver) self.conf.add_constant("py_impl", "python implementation", value=self.py_impl) + self.conf.add_constant("py_free_threaded", "is no-gil interpreted", value=self.py_free_threaded) def _default_set_env(self) -> dict[str, str]: env = super()._default_set_env() @@ -111,6 +119,9 @@ def py_dot_ver(self) -> str: return self.base_python.version_dot + def py_free_threaded(self) -> bool: + return self.base_python.free_threaded + def py_impl(self) -> str: return self.base_python.impl_lower @@ -145,7 +156,7 @@ match = PY_FACTORS_RE_EXPLICIT_VERSION.match(env_name) if match: found = match.groupdict() - candidates.append(f"{'pypy' if found['impl'] == 'pypy' else ''}{found['version']}") + candidates.append(f"{'pypy' if found['impl'] == 'pypy' else ''}{found['version']}{found['threaded']}") else: for factor in env_name.split("-"): match = PY_FACTORS_RE.match(factor) @@ -163,7 +174,8 @@ implementation = sys.implementation.name version = sys.version_info bits = "64" if sys.maxsize > 2**32 else "32" - string_spec = f"{implementation}{version.major}{version.minor}-{bits}" + threaded = "t" if sysconfig.get_config_var("Py_GIL_DISABLED") == 1 else "" + string_spec = f"{implementation}{version.major}{version.minor}{threaded}-{bits}" return PythonSpec.from_string_spec(string_spec) @classmethod @@ -186,7 +198,7 @@ spec_base = cls.python_spec_for_path(path) if any( getattr(spec_base, key) != getattr(spec_name, key) - for key in ("implementation", "major", "minor", "micro", "architecture") + for key in ("implementation", "major", "minor", "micro", "architecture", "free_threaded") if getattr(spec_name, key) is not None ): msg = f"env name {env_name} conflicting with base python {base_python}" @@ -290,7 +302,7 @@ raise Skip(msg) raise NoInterpreter(base_pythons) - return cast("PythonInfo", self._base_python) + return self._base_python def _get_env_journal_python(self) -> dict[str, Any]: return { @@ -300,6 +312,7 @@ "is_64": self.base_python.is_64, "sysplatform": self.base_python.platform, "extra_version_info": None, + "free_threaded": self.base_python.free_threaded, } @abstractmethod diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/src/tox/tox_env/python/dependency_groups.py new/tox-4.27.0/src/tox/tox_env/python/dependency_groups.py --- old/tox-4.25.0/src/tox/tox_env/python/dependency_groups.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/src/tox/tox_env/python/dependency_groups.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,7 @@ from __future__ import annotations import sys +from collections import defaultdict from typing import TYPE_CHECKING, TypedDict from packaging.requirements import InvalidRequirement, Requirement @@ -26,28 +27,64 @@ return set() with pyproject_file.open("rb") as file_handler: pyproject = tomllib.load(file_handler) - dependency_groups = pyproject["dependency-groups"] - if not isinstance(dependency_groups, dict): - msg = f"dependency-groups is {type(dependency_groups).__name__} instead of table" + dependency_groups_raw = pyproject["dependency-groups"] + if not isinstance(dependency_groups_raw, dict): + msg = f"dependency-groups is {type(dependency_groups_raw).__name__} instead of table" raise Fail(msg) + original_names_lookup, dependency_groups = _normalize_group_names(dependency_groups_raw) result: set[Requirement] = set() for group in groups: - result = result.union(_resolve_dependency_group(dependency_groups, group)) + result = result.union(_resolve_dependency_group(dependency_groups, group, original_names_lookup)) return result +def _normalize_group_names( + dependency_groups: dict[str, list[str] | _IncludeGroup], +) -> tuple[dict[str, str], dict[str, list[str] | _IncludeGroup]]: + original_names = defaultdict(list) + normalized_groups = {} + + for group_name, value in dependency_groups.items(): + normed_group_name: str = canonicalize_name(group_name) + original_names[normed_group_name].append(group_name) + normalized_groups[normed_group_name] = value + + errors = [] + for normed_name, names in original_names.items(): + if len(names) > 1: + errors.append(f"{normed_name} ({', '.join(names)})") + if errors: + msg = f"Duplicate dependency group names: {', '.join(errors)}" + raise ValueError(msg) + + original_names_lookup = { + normed_name: original_names[0] + for normed_name, original_names in original_names.items() + if len(original_names) == 1 + } + + return original_names_lookup, normalized_groups + + def _resolve_dependency_group( - dependency_groups: dict[str, list[str] | _IncludeGroup], group: str, past_groups: tuple[str, ...] = () + dependency_groups: dict[str, list[str] | _IncludeGroup], + group: str, + original_names_lookup: dict[str, str], + past_groups: tuple[str, ...] = (), ) -> set[Requirement]: if group in past_groups: - msg = f"Cyclic dependency group include: {group!r} -> {past_groups!r}" + original_group = original_names_lookup.get(group, group) + original_past_groups = tuple(original_names_lookup.get(g, g) for g in past_groups) + msg = f"Cyclic dependency group include: {original_group!r} -> {original_past_groups!r}" raise Fail(msg) if group not in dependency_groups: - msg = f"dependency group {group!r} not found" + original_group = original_names_lookup.get(group, group) + msg = f"dependency group {original_group!r} not found" raise Fail(msg) raw_group = dependency_groups[group] if not isinstance(raw_group, list): - msg = f"dependency group {group!r} is not a list" + original_group = original_names_lookup.get(group, group) + msg = f"dependency group {original_group!r} is not a list" raise Fail(msg) result = set() @@ -63,7 +100,11 @@ raise Fail(msg) from exc elif isinstance(item, dict) and tuple(item.keys()) == ("include-group",): include_group = canonicalize_name(next(iter(item.values()))) - result = result.union(_resolve_dependency_group(dependency_groups, include_group, (*past_groups, group))) + result = result.union( + _resolve_dependency_group( + dependency_groups, include_group, original_names_lookup, (*past_groups, group) + ) + ) else: msg = f"invalid dependency group item: {item!r}" raise Fail(msg) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/src/tox/tox_env/python/virtual_env/api.py new/tox-4.27.0/src/tox/tox_env/python/virtual_env/api.py --- old/tox-4.25.0/src/tox/tox_env/python/virtual_env/api.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/src/tox/tox_env/python/virtual_env/api.py 2020-02-02 01:00:00.000000000 +0100 @@ -146,6 +146,7 @@ is_64=(interpreter.architecture == 64), # noqa: PLR2004 platform=interpreter.platform, extra={"executable": Path(interpreter.system_executable).resolve()}, + free_threaded=interpreter.free_threaded, ) def prepend_env_var_path(self) -> list[Path]: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/src/tox/version.py new/tox-4.27.0/src/tox/version.py --- old/tox-4.25.0/src/tox/version.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/src/tox/version.py 2020-02-02 01:00:00.000000000 +0100 @@ -17,5 +17,5 @@ __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE -__version__ = version = '4.25.0' -__version_tuple__ = version_tuple = (4, 25, 0) +__version__ = version = '4.27.0' +__version_tuple__ = version_tuple = (4, 27, 0) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/tests/config/test_set_env.py new/tox-4.27.0/tests/config/test_set_env.py --- old/tox-4.25.0/tests/config/test_set_env.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/tests/config/test_set_env.py 2020-02-02 01:00:00.000000000 +0100 @@ -240,3 +240,51 @@ result = project.run("r") result.assert_failed() assert f"py: failed with {project.path / 'magic.txt'} does not exist for set_env" in result.out + + +# https://github.com/tox-dev/tox/issues/2435 +def test_set_env_environment_with_file_and_expanded_substitution( + tox_project: ToxProjectCreator, monkeypatch: MonkeyPatch +) -> None: + conf = { + "tox.ini": """ + [tox] + envlist = + check + + [testenv] + setenv = + file|.env + PRECENDENCE_TEST_1=1_expanded_precedence + + [testenv:check] + setenv = + {[testenv]setenv} + PRECENDENCE_TEST_1=1_self_precedence + PRECENDENCE_TEST_2=2_self_precedence + """, + ".env": """ + PRECENDENCE_TEST_1=1_file_precedence + PRECENDENCE_TEST_2=2_file_precedence + PRECENDENCE_TEST_3=3_file_precedence + """, + } + monkeypatch.setenv("env_file", ".env") + project = tox_project(conf) + + result = project.run("c", "-k", "set_env", "-e", "check") + result.assert_success() + set_env = result.env_conf("check")["set_env"] + content = {k: set_env.load(k) for k in set_env} + assert content == { + "PIP_DISABLE_PIP_VERSION_CHECK": "1", + "PYTHONHASHSEED": ANY, + "PYTHONIOENCODING": "utf-8", + "PRECENDENCE_TEST_1": "1_expanded_precedence", + "PRECENDENCE_TEST_2": "2_self_precedence", + "PRECENDENCE_TEST_3": "3_file_precedence", + } + + result = project.run("r", "-e", "check") + result.assert_success() + assert "check: OK" in result.out diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/tests/conftest.py new/tox-4.27.0/tests/conftest.py --- old/tox-4.25.0/tests/conftest.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/tests/conftest.py 2020-02-02 01:00:00.000000000 +0100 @@ -2,6 +2,7 @@ import os import sys +import sysconfig from pathlib import Path from typing import TYPE_CHECKING, Callable, Iterator, Protocol, Sequence from unittest.mock import patch @@ -100,6 +101,7 @@ is_64=True, platform=sys.platform, extra={"executable": Path(sys.executable)}, + free_threaded=sysconfig.get_config_var("Py_GIL_DISABLED") == 1, ) mocker.patch.object(VirtualEnv, "_get_python", get_python) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/tests/session/cmd/test_sequential.py new/tox-4.27.0/tests/session/cmd/test_sequential.py --- old/tox-4.25.0/tests/session/cmd/test_sequential.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/tests/session/cmd/test_sequential.py 2020-02-02 01:00:00.000000000 +0100 @@ -89,6 +89,7 @@ "sysplatform": py_info.platform, "version": py_info.version, "version_info": list(py_info.version_info), + "free_threaded": py_info.free_threaded, } packaging_setup = get_cmd_exit_run_id(log_report, ".pkg", "setup") assert "result" not in log_report["testenvs"][".pkg"] @@ -114,7 +115,9 @@ py_test = get_cmd_exit_run_id(log_report, "py", "test") assert py_test == [(1, "commands[0]"), (0, "commands[1]")] packaging_installed = log_report["testenvs"]["py"].pop("installed_packages") - expected_pkg = {"pip", "setuptools", "wheel", "a"} + expected_pkg = {"pip", "setuptools", "a"} + if sys.version_info[0:2] == (3, 8): + expected_pkg.add("wheel") assert {i[: i.find("==")] if "@" not in i else "a" for i in packaging_installed} == expected_pkg install_package = log_report["testenvs"]["py"].pop("installpkg") assert re.match(r"^[a-fA-F0-9]{64}$", install_package.pop("sha256")) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/tests/session/cmd/test_show_config.py new/tox-4.27.0/tests/session/cmd/test_show_config.py --- old/tox-4.25.0/tests/session/cmd/test_show_config.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/tests/session/cmd/test_show_config.py 2020-02-02 01:00:00.000000000 +0100 @@ -135,7 +135,7 @@ + (["PROGRAMDATA"] if is_win else []) + (["PROGRAMFILES"] if is_win else []) + (["PROGRAMFILES(x86)"] if is_win else []) - + ["REQUESTS_CA_BUNDLE", "SSL_CERT_FILE"] + + ["PYTHON_GIL", "REQUESTS_CA_BUNDLE", "SSL_CERT_FILE"] + (["SYSTEMDRIVE", "SYSTEMROOT", "TEMP"] if is_win else []) + (["TERM"] if stdout_is_atty else []) + (["TMP", "USERPROFILE"] if is_win else ["TMPDIR"]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/tests/session/test_env_select.py new/tox-4.27.0/tests/session/test_env_select.py --- old/tox-4.25.0/tests/session/test_env_select.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/tests/session/test_env_select.py 2020-02-02 01:00:00.000000000 +0100 @@ -261,10 +261,15 @@ "pypy312", "py3", "py3.12", + "py3.12t", "py312", + "py312t", "3", + "3t", "3.12", + "3.12t", "3.12.0", + "3.12.0t", ], ) def test_dynamic_env_factors_match(env: str) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/tests/tox_env/python/test_python_api.py new/tox-4.27.0/tests/tox_env/python/test_python_api.py --- old/tox-4.25.0/tests/tox_env/python/test_python_api.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/tests/tox_env/python/test_python_api.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,7 @@ from __future__ import annotations import sys +import sysconfig from types import SimpleNamespace from typing import TYPE_CHECKING, Callable from unittest.mock import patch @@ -81,30 +82,56 @@ ("env", "base_python"), [ ("py3", "py3"), + ("py3t", "py3t"), ("py311", "py311"), + ("py311t", "py311t"), ("py3.12", "py3.12"), + ("py3.12t", "py3.12t"), ("pypy2", "pypy2"), + ("pypy2t", "pypy2t"), ("rustpython3", "rustpython3"), + ("rustpython3t", "rustpython3t"), ("graalpy", "graalpy"), + ("graalpyt", None), ("jython", "jython"), + ("jythont", None), ("cpython3.8", "cpython3.8"), + ("cpython3.8t", "cpython3.8t"), ("ironpython2.7", "ironpython2.7"), + ("ironpython2.7t", "ironpython2.7t"), ("functional-py310", "py310"), + ("functional-py310t", "py310t"), ("bar-pypy2-foo", "pypy2"), + ("bar-foo2t-py2", "py2"), + ("bar-pypy2t-foo", "pypy2t"), ("py", None), + ("pyt", None), ("django-32", None), + ("django-32t", None), ("eslint-8.3", None), + ("eslint-8.3t", None), ("py-310", None), + ("py-310t", None), ("py3000", None), + ("py3000t", None), ("4.foo", None), + ("4.foot", None), ("310", None), + ("310t", None), ("5", None), + ("5t", None), ("2000", None), + ("2000t", None), ("4000", None), + ("4000t", None), ("3.10", "3.10"), + ("3.10t", "3.10t"), ("3.9", "3.9"), + ("3.9t", "3.9t"), ("2.7", "2.7"), + ("2.7t", "2.7t"), ("pypy-3.10", "pypy3.10"), + ("pypy-3.10t", "pypy3.10t"), ], ids=lambda a: "|".join(a) if isinstance(a, list) else str(a), ) @@ -294,13 +321,24 @@ @pytest.mark.parametrize( - ("impl", "major", "minor", "arch"), + ("impl", "major", "minor", "arch", "free_threaded"), [ - ("cpython", 3, 12, 64), - ("pypy", 3, 9, 32), + ("cpython", 3, 12, 64, None), + ("cpython", 3, 13, 64, True), + ("cpython", 3, 13, 64, False), + ("pypy", 3, 9, 32, None), ], ) -def test_python_spec_for_sys_executable(impl: str, major: int, minor: int, arch: int, mocker: MockerFixture) -> None: +def test_python_spec_for_sys_executable( # noqa: PLR0913 + impl: str, major: int, minor: int, arch: int, free_threaded: bool | None, mocker: MockerFixture +) -> None: + get_config_var_ = sysconfig.get_config_var + + def get_config_var(name: str) -> object: + if name == "Py_GIL_DISABLED": + return free_threaded + return get_config_var_(name) + version_info = SimpleNamespace(major=major, minor=minor, micro=5, releaselevel="final", serial=0) implementation = SimpleNamespace( name=impl, @@ -312,8 +350,10 @@ mocker.patch.object(sys, "version_info", version_info) mocker.patch.object(sys, "implementation", implementation) mocker.patch.object(sys, "maxsize", 2**arch // 2 - 1) + mocker.patch.object(sysconfig, "get_config_var", get_config_var) spec = Python._python_spec_for_sys_executable() # noqa: SLF001 assert spec.implementation == impl assert spec.major == major assert spec.minor == minor assert spec.architecture == arch + assert spec.free_threaded == bool(free_threaded) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/tests/tox_env/python/test_python_runner.py new/tox-4.27.0/tests/tox_env/python/test_python_runner.py --- old/tox-4.25.0/tests/tox_env/python/test_python_runner.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/tests/tox_env/python/test_python_runner.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,7 @@ from __future__ import annotations import sys +import sysconfig from pathlib import Path from typing import TYPE_CHECKING @@ -152,11 +153,16 @@ assert result.code == (0 if expected else -1) +SYS_PY_VER = "".join(str(i) for i in sys.version_info[0:2]) + ( + "t" if sysconfig.get_config_var("Py_GIL_DISABLED") == 1 else "" +) + + @pytest.mark.parametrize( ("skip", "env", "retcode"), [ - ("true", f"py{''.join(str(i) for i in sys.version_info[0:2])}", 0), - ("false", f"py{''.join(str(i) for i in sys.version_info[0:2])}", 0), + ("true", f"py{SYS_PY_VER}", 0), + ("false", f"py{SYS_PY_VER}", 0), ("true", "py31", -1), ("false", "py31", 1), ("true", None, 0), @@ -169,8 +175,7 @@ env: str | None, retcode: int, ) -> None: - py_ver = "".join(str(i) for i in sys.version_info[0:2]) - project = tox_project({"tox.ini": f"[tox]\nenvlist=py31,py{py_ver}\n[testenv]\nusedevelop=true"}) + project = tox_project({"tox.ini": f"[tox]\nenvlist=py31,py{SYS_PY_VER}\n[testenv]\nusedevelop=true"}) args = [f"--skip-missing-interpreters={skip}"] if env: args += ["-e", env] @@ -256,8 +261,12 @@ "furo>=2024.8.6", "sphinx>=8.0.2", ] + "friendly.Bard" = [ + "bard-song", + ] type = [ {include-group = "test"}, + {include-group = "FrIeNdLy-._.-bArD"}, "mypy>=1", ] """, @@ -273,7 +282,7 @@ ( "py", "install_dependency-groups", - ["python", "-I", "-m", "pip", "install", "furo>=2024.8.6", "mypy>=1", "sphinx>=8.0.2"], + ["python", "-I", "-m", "pip", "install", "bard-song", "furo>=2024.8.6", "mypy>=1", "sphinx>=8.0.2"], ) ] @@ -325,18 +334,18 @@ "tox.toml": """ [env_run_base] skip_install = true - dependency_groups = ["test"] + dependency_groups = ["tEst"] """, "pyproject.toml": """ [dependency-groups] - test = 1 + teSt = 1 """, }, ) result = project.run("r", "-e", "py") result.assert_failed() - assert "py: failed with dependency group 'test' is not a list\n" in result.out + assert "py: failed with dependency group 'teSt' is not a list\n" in result.out def test_dependency_groups_bad_requirement(tox_project: ToxProjectCreator) -> None: @@ -393,12 +402,12 @@ """, "pyproject.toml": """ [dependency-groups] - test = [ { include-group = "type" } ] - type = [ { include-group = "test" } ] + teSt = [ { include-group = "type" } ] + tyPe = [ { include-group = "test" } ] """, }, ) result = project.run("r", "-e", "py") result.assert_failed() - assert "py: failed with Cyclic dependency group include: 'test' -> ('test', 'type')\n" in result.out + assert "py: failed with Cyclic dependency group include: 'teSt' -> ('teSt', 'tyPe')\n" in result.out diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/tests/tox_env/test_api.py new/tox-4.27.0/tests/tox_env/test_api.py --- old/tox-4.25.0/tests/tox_env/test_api.py 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/tests/tox_env/test_api.py 2020-02-02 01:00:00.000000000 +0100 @@ -2,6 +2,10 @@ from typing import TYPE_CHECKING +import pytest + +from tox.tox_env.api import redact_value + if TYPE_CHECKING: from pathlib import Path @@ -32,3 +36,31 @@ project = tox_project({"tox.ini": ini}) result = project.run() result.assert_success() + + [email protected]( + ("key", "do_redact"), + [ + pytest.param("SOME_KEY", True, id="key"), + pytest.param("API_FOO", True, id="api"), + pytest.param("AUTH", True, id="auth"), + pytest.param("CLIENT", True, id="client"), + pytest.param("DB_PASSWORD", True, id="password"), + pytest.param("FOO", False, id="foo"), + pytest.param("GITHUB_TOKEN", True, id="token"), + pytest.param("NORMAL_VAR", False, id="other"), + pytest.param("S_PASSWD", True, id="passwd"), + pytest.param("SECRET", True, id="secret"), + pytest.param("SOME_ACCESS", True, id="access"), + pytest.param("MY_CRED", True, id="cred"), + pytest.param("MY_PRIVATE", True, id="private"), + pytest.param("MY_PWD", True, id="pwd"), + ], +) +def test_redact(key: str, do_redact: bool) -> None: + """Ensures that redact_value works as expected.""" + result = redact_value(key, "foo") + if do_redact: + assert result == "***" + else: + assert result == "foo" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tox-4.25.0/tox.toml new/tox-4.27.0/tox.toml --- old/tox-4.25.0/tox.toml 2020-02-02 01:00:00.000000000 +0100 +++ new/tox-4.27.0/tox.toml 2020-02-02 01:00:00.000000000 +0100 @@ -1,5 +1,5 @@ requires = ["tox>=4.24.1"] -env_list = ["fix", "3.13", "3.12", "3.11", "3.10", "3.9", "3.8", "cov", "type", "docs", "pkg_meta"] +env_list = ["fix", "3.14t", "3.14", "3.13", "3.12", "3.11", "3.10", "3.9", "cov", "type", "docs", "pkg_meta"] skip_missing_interpreters = true [env_run_base]
