Skip to content

Commit 5cef030

Browse files
authored
Fix python hash seed not being set (#2739)
Resolves #2645
1 parent d074f3f commit 5cef030

File tree

8 files changed

+100
-15
lines changed

8 files changed

+100
-15
lines changed

docs/changelog/2645.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix python hash seed not being set - by :user:`gaborbernat`.

src/tox/session/cmd/run/common.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import logging
55
import os
6+
import random
7+
import sys
68
import time
79
from argparse import Action, ArgumentError, ArgumentParser, Namespace
810
from concurrent.futures import CancelledError, Future, ThreadPoolExecutor, as_completed
@@ -108,14 +110,35 @@ def env_run_create_flags(parser: ArgumentParser, mode: str) -> None:
108110
help="install package in development mode",
109111
dest="develop",
110112
)
111-
if mode not in ("config", "depends"):
113+
if mode not in ("depends",):
114+
115+
class SeedAction(Action):
116+
def __call__(
117+
self,
118+
parser: ArgumentParser, # noqa: U100
119+
namespace: Namespace,
120+
values: str | Sequence[Any] | None,
121+
option_string: str | None = None, # noqa: U100
122+
) -> None:
123+
if values == "notset":
124+
result = None
125+
else:
126+
try:
127+
result = int(cast(str, values))
128+
if result <= 0:
129+
raise ValueError("must be greater than zero")
130+
except ValueError as exc:
131+
raise ArgumentError(self, str(exc))
132+
setattr(namespace, self.dest, result)
133+
112134
parser.add_argument(
113135
"--hashseed",
114136
metavar="SEED",
115137
help="set PYTHONHASHSEED to SEED before running commands. Defaults to a random integer in the range "
116138
"[1, 4294967295] ([1, 1024] on Windows). Passing 'noset' suppresses this behavior.",
117-
type=str,
118-
default="noset",
139+
action=SeedAction,
140+
of_type=Optional[int],
141+
default=random.randint(1, 1024 if sys.platform == "win32" else 4294967295),
119142
dest="hash_seed",
120143
)
121144
parser.add_argument(

src/tox/tox_env/python/api.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ def validate_base_python(value: list[str]) -> list[str]:
8888
self.conf.add_constant("py_dot_ver", "<python major>.<python minor>", value=self.py_dot_ver)
8989
self.conf.add_constant("py_impl", "python implementation", value=self.py_impl)
9090

91+
def _default_set_env(self) -> dict[str, str]:
92+
env = super()._default_set_env()
93+
hash_seed: int | None = getattr(self.options, "hash_seed", None)
94+
if hash_seed is not None:
95+
env["PYTHONHASHSEED"] = str(hash_seed)
96+
return env
97+
9198
def py_dot_ver(self) -> str:
9299
return self.base_python.version_dot
93100

tests/config/cli/test_cli_env_var.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from typing import Callable
4+
from unittest.mock import ANY
45

56
import pytest
67

@@ -53,7 +54,7 @@ def test_verbose_no_test() -> None:
5354
"package_only": False,
5455
"install_pkg": None,
5556
"develop": False,
56-
"hash_seed": "noset",
57+
"hash_seed": ANY,
5758
"discover": [],
5859
"parallel": 0,
5960
"parallel_live": False,
@@ -91,7 +92,7 @@ def test_env_var_exhaustive_parallel_values(
9192
"discover": [],
9293
"env": CliEnv(["py37", "py36"]),
9394
"force_dep": [],
94-
"hash_seed": "noset",
95+
"hash_seed": ANY,
9596
"install_pkg": None,
9697
"no_provision": False,
9798
"list_envs": False,

tests/config/cli/test_cli_ini.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import textwrap
66
from pathlib import Path
77
from typing import Any, Callable
8+
from unittest.mock import ANY
89

910
import pytest
1011
from pytest_mock import MockerFixture
@@ -67,6 +68,7 @@ def test_ini_empty(
6768

6869
to.unlink()
6970
missing_options = get_options("r")
71+
missing_options.parsed.hash_seed = ANY
7072
assert vars(missing_options.parsed) == vars(options.parsed)
7173

7274

@@ -79,7 +81,7 @@ def default_options(tmp_path: Path) -> dict[str, Any]:
7981
"develop": False,
8082
"discover": [],
8183
"env": CliEnv(),
82-
"hash_seed": "noset",
84+
"hash_seed": ANY,
8385
"install_pkg": None,
8486
"no_test": False,
8587
"override": [],
@@ -112,7 +114,7 @@ def test_ini_exhaustive_parallel_values(exhaustive_ini: Path, core_handlers: dic
112114
"develop": False,
113115
"discover": [],
114116
"env": CliEnv(["py37", "py36"]),
115-
"hash_seed": "noset",
117+
"hash_seed": ANY,
116118
"install_pkg": None,
117119
"no_test": True,
118120
"override": [Override("a=b"), Override("c=d")],

tests/config/test_set_env.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sys
44
from pathlib import Path
55
from typing import Any
6+
from unittest.mock import ANY
67

78
import pytest
89
from pytest_mock import MockerFixture
@@ -60,9 +61,9 @@ def func(tox_ini: str, extra_files: dict[str, Any] | None = None, from_cwd: Path
6061
def test_set_env_default(eval_set_env: EvalSetEnv) -> None:
6162
set_env = eval_set_env("")
6263
keys = list(set_env)
63-
assert keys == ["PIP_DISABLE_PIP_VERSION_CHECK", "PYTHONIOENCODING"]
64+
assert keys == ["PYTHONHASHSEED", "PIP_DISABLE_PIP_VERSION_CHECK", "PYTHONIOENCODING"]
6465
values = [set_env.load(k) for k in keys]
65-
assert values == ["1", "utf-8"]
66+
assert values == [ANY, "1", "utf-8"]
6667

6768

6869
def test_set_env_self_key(eval_set_env: EvalSetEnv, monkeypatch: MonkeyPatch) -> None:
@@ -120,7 +121,13 @@ def test_set_env_replacer(eval_set_env: EvalSetEnv, monkeypatch: MonkeyPatch) ->
120121
monkeypatch.setenv("MAGIC", "\nb=2\n")
121122
set_env = eval_set_env("[testenv]\npackage=skip\nset_env=a=1\n {env:MAGIC}")
122123
env = {k: set_env.load(k) for k in set_env}
123-
assert env == {"PIP_DISABLE_PIP_VERSION_CHECK": "1", "a": "1", "b": "2", "PYTHONIOENCODING": "utf-8"}
124+
assert env == {
125+
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
126+
"a": "1",
127+
"b": "2",
128+
"PYTHONIOENCODING": "utf-8",
129+
"PYTHONHASHSEED": ANY,
130+
}
124131

125132

126133
def test_set_env_honor_override(eval_set_env: EvalSetEnv) -> None:
@@ -143,6 +150,7 @@ def test_set_env_environment_file(eval_set_env: EvalSetEnv) -> None:
143150
content = {k: set_env.load(k) for k in set_env}
144151
assert content == {
145152
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
153+
"PYTHONHASHSEED": ANY,
146154
"A": "1",
147155
"B": "2",
148156
"C": "1",

tests/tox_env/python/test_python_api.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,51 @@ def test_base_python_env_conflict_show_conf(tox_project: ToxProjectCreator, igno
131131
f" base python py{py_ver_next}'{',' if comma_in_exc else ''})\n"
132132
)
133133
result.assert_out_err(out, "")
134+
135+
136+
def test_python_set_hash_seed(tox_project: ToxProjectCreator) -> None:
137+
ini = "[testenv]\npackage=skip\ncommands=python -c 'import os; print(os.environ[\"PYTHONHASHSEED\"])'"
138+
prj = tox_project({"tox.ini": ini})
139+
result = prj.run("r", "-e", "py", "--hashseed", "10")
140+
result.assert_success()
141+
assert result.out.splitlines()[1] == "10"
142+
143+
144+
def test_python_generate_hash_seed(tox_project: ToxProjectCreator) -> None:
145+
ini = "[testenv]\npackage=skip\ncommands=python -c 'import os; print(os.environ[\"PYTHONHASHSEED\"])'"
146+
prj = tox_project({"tox.ini": ini})
147+
result = prj.run("r", "-e", "py")
148+
result.assert_success()
149+
assert 1 <= int(result.out.splitlines()[1]) <= (1024 if sys.platform == "win32" else 4294967295)
150+
151+
152+
def test_python_keep_hash_seed(tox_project: ToxProjectCreator) -> None:
153+
ini = """
154+
[testenv]
155+
package=skip
156+
set_env=PYTHONHASHSEED=12
157+
commands=python -c 'import os; print(os.environ["PYTHONHASHSEED"])'
158+
"""
159+
result = tox_project({"tox.ini": ini}).run("r", "-e", "py")
160+
result.assert_success()
161+
assert result.out.splitlines()[1] == "12"
162+
163+
164+
def test_python_disable_hash_seed(tox_project: ToxProjectCreator) -> None:
165+
ini = "[testenv]\npackage=skip\ncommands=python -c 'import os; print(os.environ.get(\"PYTHONHASHSEED\"))'"
166+
prj = tox_project({"tox.ini": ini})
167+
result = prj.run("r", "-e", "py", "--hashseed", "notset")
168+
result.assert_success()
169+
assert result.out.splitlines()[1] == "None"
170+
171+
172+
def test_python_set_hash_seed_negative(tox_project: ToxProjectCreator) -> None:
173+
result = tox_project({"tox.ini": ""}).run("r", "-e", "py", "--hashseed", "-1")
174+
result.assert_failed(2)
175+
assert "tox run: error: argument --hashseed: must be greater than zero" in result.err
176+
177+
178+
def test_python_set_hash_seed_incorrect(tox_project: ToxProjectCreator) -> None:
179+
result = tox_project({"tox.ini": ""}).run("r", "-e", "py", "--hashseed", "ok")
180+
result.assert_failed(2)
181+
assert "tox run: error: argument --hashseed: invalid literal for int() with base 10: 'ok'" in result.err

tox.ini

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,6 @@ commands =
4848
pre-commit run --all-files --show-diff-on-failure {tty:--color=always} {posargs}
4949
python -c 'print(r"hint: run {envbindir}{/}pre-commit install to add checks as pre-commit hook")'
5050

51-
[testenv:py311]
52-
setenv =
53-
{[testenv]setenv}
54-
AIOHTTP_NO_EXTENSIONS = 1
55-
5651
[testenv:type]
5752
description = run type check on code base
5853
setenv =

0 commit comments

Comments
 (0)