Skip to content

Commit 89f9f76

Browse files
authored
Add PyPy support (#51)
1 parent ca592d6 commit 89f9f76

File tree

7 files changed

+136
-33
lines changed

7 files changed

+136
-33
lines changed

.github/workflows/check.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ jobs:
2929
- ubuntu-latest
3030
- windows-latest
3131
- macos-latest
32+
include:
33+
- { os: ubuntu-latest, py: "pypy3.10" }
34+
- { os: ubuntu-latest, py: "pypy3.8" }
35+
- { os: windows-latest, py: "pypy3.10" }
36+
- { os: windows-latest, py: "pypy3.8" }
3237
steps:
3338
- name: setup python for tox
3439
uses: actions/setup-python@v5

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ repos:
2323
rev: "1.8.0"
2424
hooks:
2525
- id: pyproject-fmt
26-
additional_dependencies: ["tox>=4.14"]
26+
additional_dependencies: ["tox>=4.15"]
2727
- repo: https://github.com/astral-sh/ruff-pre-commit
28-
rev: "v0.4.1"
28+
rev: "v0.4.2"
2929
hooks:
3030
- id: ruff-format
3131
- id: ruff

pyproject.toml

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
build-backend = "hatchling.build"
33
requires = [
44
"hatch-vcs>=0.4",
5-
"hatchling>=1.21.1",
5+
"hatchling>=1.24.2",
66
]
77

88
[project]
@@ -16,7 +16,9 @@ keywords = [
1616
"virtual",
1717
]
1818
license = "MIT"
19-
maintainers = [{ name = "Bernát Gábor", email = "[email protected]" }]
19+
maintainers = [
20+
{ name = "Bernát Gábor", email = "[email protected]" },
21+
]
2022
requires-python = ">=3.8"
2123
classifiers = [
2224
"Development Status :: 5 - Production/Stable",
@@ -38,17 +40,19 @@ dynamic = [
3840
"version",
3941
]
4042
dependencies = [
41-
"packaging>=23.2",
42-
"tox<5,>=4.14",
43-
"uv<1,>=0.1.15",
43+
'importlib_resources>=6.4; python_version < "3.9"',
44+
"packaging>=24",
45+
"tox<5,>=4.15",
46+
"uv<1,>=0.1.39",
4447
]
4548
optional-dependencies.test = [
4649
"covdefaults>=2.3",
4750
"devpi-process>=1",
48-
"pytest>=8.0.2",
49-
"pytest-cov>=4.1",
50-
"pytest-mock>=3.12",
51+
"pytest>=8.2",
52+
"pytest-cov>=5",
53+
"pytest-mock>=3.14",
5154
]
55+
urls.Changelog = "https://github.com/tox-dev/tox-uv/releases"
5256
urls.Documentation = "https://github.com/tox-dev/tox-uv#tox-uv"
5357
urls.Homepage = "https://github.com/tox-dev/tox-uv"
5458
urls.Source = "https://github.com/tox-dev/tox-uv"
@@ -57,7 +61,10 @@ entry-points.tox = {"tox-uv" = "tox_uv.plugin"}
5761

5862
[tool.hatch]
5963
build.hooks.vcs.version-file = "src/tox_uv/version.py"
60-
build.targets.sdist.include = ["/src", "/tests"]
64+
build.targets.sdist.include = [
65+
"/src",
66+
"/tests",
67+
]
6168
version.source = "vcs"
6269

6370
[tool.black]
@@ -66,8 +73,15 @@ line-length = 120
6673
[tool.ruff]
6774
line-length = 120
6875
target-version = "py38"
69-
lint.isort = { known-first-party = ["tox_uv", "tests"], required-imports = ["from __future__ import annotations"] }
70-
lint.select = ["ALL"]
76+
lint.isort = { known-first-party = [
77+
"tox_uv",
78+
"tests",
79+
], required-imports = [
80+
"from __future__ import annotations",
81+
] }
82+
lint.select = [
83+
"ALL",
84+
]
7185
lint.ignore = [
7286
"ANN101", # Missing type annotation for `self` in method
7387
"D301", # Use `r"""` if any backslashes in a docstring
@@ -103,14 +117,34 @@ count = true
103117
[tool.coverage]
104118
html.show_contexts = true
105119
html.skip_covered = false
106-
paths.source = ["src", ".tox/*/.venv/lib/*/site-packages", ".tox\\*\\.venv\\Lib\\site-packages", "**/src", "**\\src"]
107-
paths.other = [".", "*/tox_uv", "*\\tox_uv"]
120+
paths.source = [
121+
"src",
122+
".tox/*/.venv/lib/*/site-packages",
123+
".tox\\*\\.venv\\Lib\\site-packages",
124+
"**/src",
125+
"**\\src",
126+
]
127+
paths.other = [
128+
".",
129+
"*/tox_uv",
130+
"*\\tox_uv",
131+
]
132+
report.omit = [
133+
"src/tox_uv/_venv_query.py",
134+
]
108135
report.fail_under = 100
109136
run.parallel = true
110-
run.plugins = ["covdefaults"]
137+
run.plugins = [
138+
"covdefaults",
139+
]
111140

112141
[tool.mypy]
113142
python_version = "3.11"
114143
show_error_codes = true
115144
strict = true
116-
overrides = [{ module = ["virtualenv.*", "uv.*"], ignore_missing_imports = true }]
145+
overrides = [
146+
{ module = [
147+
"virtualenv.*",
148+
"uv.*",
149+
], ignore_missing_imports = true },
150+
]

src/tox_uv/_venv.py

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,17 @@
22

33
from __future__ import annotations
44

5+
import json
56
import sys
67
from abc import ABC
8+
from functools import cached_property
9+
10+
if sys.version_info >= (3, 9): # pragma: no cover (py39+)
11+
from importlib.resources import as_file, files
12+
else: # pragma: no cover (py38+)
13+
from importlib_resources import as_file, files
14+
15+
716
from pathlib import Path
817
from platform import python_implementation
918
from typing import TYPE_CHECKING, Any, cast
@@ -26,6 +35,7 @@ class UvVenv(Python, ABC):
2635
def __init__(self, create_args: ToxEnvCreateArgs) -> None:
2736
self._executor: Execute | None = None
2837
self._installer: UvInstaller | None = None
38+
self._created = False
2939
super().__init__(create_args)
3040

3141
def register_config(self) -> None:
@@ -112,14 +122,12 @@ def _default_pass_env(self) -> list[str]:
112122
return env
113123

114124
def create_python_env(self) -> None:
115-
base = self.base_python.version_info
116-
version_spec = (
117-
sys.executable
118-
if (base.major, base.minor) == sys.version_info[:2]
119-
else f"{base.major}.{base.minor}"
120-
if base.minor
121-
else f"{base.major}"
122-
)
125+
base, imp = self.base_python.version_info, self.base_python.impl_lower
126+
if (base.major, base.minor) == sys.version_info[:2] and (sys.implementation.name.lower() == imp):
127+
version_spec = sys.executable
128+
else:
129+
uv_imp = "python" if (imp and imp == "cpython") else imp
130+
version_spec = f"{uv_imp or ''}{base.major}.{base.minor}" if base.minor else f"{uv_imp or ''}{base.major}"
123131
cmd: list[str] = [self.uv, "venv", "-p", version_spec]
124132
if self.options.verbosity > 2: # noqa: PLR2004
125133
cmd.append("-v")
@@ -128,6 +136,7 @@ def create_python_env(self) -> None:
128136
cmd.append(str(self.venv_dir))
129137
outcome = self.execute(cmd, stdin=StdinSource.OFF, run_id="venv", show=None)
130138
outcome.assert_success()
139+
self._created = True
131140

132141
@property
133142
def _allow_externals(self) -> list[str]:
@@ -152,9 +161,34 @@ def env_site_package_dir(self) -> Path:
152161
if sys.platform == "win32": # pragma: win32 cover
153162
return self.venv_dir / "Lib" / "site-packages"
154163
else: # pragma: win32 no cover # noqa: RET505
155-
assert self.base_python.version_info.major is not None # noqa: S101
156-
assert self.base_python.version_info.minor is not None # noqa: S101
157-
return self.venv_dir / "lib" / f"python{self.base_python.version_dot}" / "site-packages"
164+
py = self._py_info
165+
impl = "pypy" if py.implementation == "pypy" else "python"
166+
return self.venv_dir / "lib" / f"{impl}{py.version_dot}" / "site-packages"
167+
168+
@cached_property
169+
def _py_info(self) -> PythonInfo: # pragma: win32 no cover
170+
if not (self._created or self.env_dir.exists()): # called during config, no environment setup
171+
self.create_python_env()
172+
self._paths = self.prepend_env_var_path()
173+
with as_file(files("tox_uv") / "_venv_query.py") as filename:
174+
cmd = [str(self.env_python()), str(filename)]
175+
outcome = self.execute(cmd, stdin=StdinSource.OFF, run_id="venv-query", show=False)
176+
outcome.assert_success()
177+
res = json.loads(outcome.out)
178+
return PythonInfo(
179+
implementation=res["implementation"],
180+
version_info=VersionInfo(
181+
major=res["version_info"][0],
182+
minor=res["version_info"][1],
183+
micro=res["version_info"][2],
184+
releaselevel=res["version_info"][3],
185+
serial=res["version_info"][4],
186+
),
187+
version=res["version"],
188+
is_64=res["is_64"],
189+
platform=sys.platform,
190+
extra={},
191+
)
158192

159193

160194
__all__ = [

src/tox_uv/_venv_query.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import sys
5+
from platform import python_implementation
6+
7+
print( # noqa: T201
8+
json.dumps({
9+
"implementation": python_implementation().lower(),
10+
"version_info": sys.version_info,
11+
"version": sys.version,
12+
"is_64": sys.maxsize > 2**32,
13+
})
14+
)

tests/test_tox_uv_venv.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def test_uv_env_python(tox_project: ToxProjectCreator) -> None:
7979
assert env_bin_dir in result.out
8080

8181

82-
def test_uv_env_site_package_dir(tox_project: ToxProjectCreator) -> None:
82+
def test_uv_env_site_package_dir_run(tox_project: ToxProjectCreator) -> None:
8383
project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands=python -c 'print(\"{envsitepackagesdir}\")'"})
8484
result = project.run("-vv")
8585
result.assert_success()
@@ -89,7 +89,23 @@ def test_uv_env_site_package_dir(tox_project: ToxProjectCreator) -> None:
8989
if sys.platform == "win32": # pragma: win32 cover
9090
path = str(env_dir / "Lib" / "site-packages")
9191
else: # pragma: win32 no cover
92-
path = str(env_dir / "lib" / f"python{ver.major}.{ver.minor}" / "site-packages")
92+
impl = "pypy" if sys.implementation.name.lower() == "pypy" else "python"
93+
path = str(env_dir / "lib" / f"{impl}{ver.major}.{ver.minor}" / "site-packages")
94+
assert path in result.out
95+
96+
97+
def test_uv_env_site_package_dir_conf(tox_project: ToxProjectCreator) -> None:
98+
project = tox_project({"tox.ini": "[testenv]\npackage=skip\ncommands={envsitepackagesdir}"})
99+
result = project.run("c", "-e", "py", "-k", "commands")
100+
result.assert_success()
101+
102+
env_dir = project.path / ".tox" / "py" / ".venv"
103+
ver = sys.version_info
104+
if sys.platform == "win32": # pragma: win32 cover
105+
path = str(env_dir / "Lib" / "site-packages")
106+
else: # pragma: win32 no cover
107+
impl = "pypy" if sys.implementation.name.lower() == "pypy" else "python"
108+
path = str(env_dir / "lib" / f"{impl}{ver.major}.{ver.minor}" / "site-packages")
93109
assert path in result.out
94110

95111

tox.ini

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ commands =
3232
description = run static analysis and style check using flake8
3333
skip_install = true
3434
deps =
35-
pre-commit>=3.6.2
35+
pre-commit>=3.7
3636
pass_env =
3737
HOMEPATH
3838
PROGRAMDATA
@@ -42,7 +42,7 @@ commands =
4242
[testenv:type]
4343
description = run type check on code base
4444
deps =
45-
mypy==1.8
45+
mypy==1.10
4646
set_env =
4747
{tty:MYPY_FORCE_COLOR = 1}
4848
commands =
@@ -53,7 +53,7 @@ commands =
5353
description = check that the package metadata is correct
5454
skip_install = true
5555
deps =
56-
build[virtualenv]>=1.1.1
56+
build[virtualenv]>=1.2.1
5757
twine>=5
5858
set_env =
5959
{tty:FORCE_COLOR = 1}

0 commit comments

Comments
 (0)