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]

Reply via email to