Skip to content

Commit 1a2631e

Browse files
authored
tests: deduplicate tests for python-based plugins (#947)
1 parent 5a1727e commit 1a2631e

File tree

6 files changed

+249
-347
lines changed

6 files changed

+249
-347
lines changed

craft_parts/plugins/uv_plugin.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,13 @@ def _get_package_install_commands(self) -> list[str]:
111111
"sync",
112112
"--no-dev",
113113
"--no-editable",
114-
*[f'--extra "{extra}"' for extra in self._options.uv_extras],
115-
*[f'--group "{group}"' for group in self._options.uv_groups],
116114
]
117115

116+
for extra in sorted(self._options.uv_extras):
117+
sync_command.extend(["--extra", extra])
118+
for group in sorted(self._options.uv_groups):
119+
sync_command.extend(["--group", group])
120+
118121
return [shlex.join(sync_command)]
119122

120123
@override

docs/reference/changelog.rst

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
Changelog
33
*********
44

5+
2.2.1 (2024-12-19)
6+
------------------
7+
8+
Bug fixes:
9+
10+
- Fix how extras and groups are parsed for the
11+
:ref:`uv plugin<craft_parts_uv_plugin>`.
12+
513
2.2.0 (2024-12-16)
614
------------------
715

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
2+
#
3+
# Copyright 2024 Canonical Ltd.
4+
#
5+
# This program is free software; you can redistribute it and/or
6+
# modify it under the terms of the GNU Lesser General Public
7+
# License version 3 as published by the Free Software Foundation.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
# Lesser General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU Lesser General Public License
15+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
from pathlib import Path
17+
from textwrap import dedent
18+
from typing import Literal
19+
20+
import pytest
21+
from craft_parts import Part, PartInfo, ProjectInfo
22+
from craft_parts.plugins.base import BasePythonPlugin, PluginProperties
23+
from overrides import override
24+
25+
26+
class FakePythonPluginProperties(PluginProperties, frozen=True):
27+
plugin: Literal["fakepy"] = "fakepy"
28+
source: str # pyright: ignore[reportGeneralTypeIssues]
29+
30+
31+
class FakePythonPlugin(BasePythonPlugin):
32+
"""A really awesome Python plugin"""
33+
34+
properties_class = FakePythonPluginProperties
35+
_options: FakePythonPluginProperties
36+
37+
@override
38+
def _get_package_install_commands(self) -> list[str]:
39+
return ['"${PARTS_PYTHON_INTERPRETER}" -m fake_pip --install']
40+
41+
42+
@pytest.fixture
43+
def plugin(new_dir):
44+
properties = FakePythonPlugin.properties_class.unmarshal({"source": "."})
45+
info = ProjectInfo(application_name="test", cache_dir=new_dir)
46+
part_info = PartInfo(project_info=info, part=Part("p1", {}))
47+
48+
return FakePythonPlugin(properties=properties, part_info=part_info)
49+
50+
51+
def get_python_build_commands(
52+
new_dir: Path, *, should_remove_symlinks: bool = False
53+
) -> list[str]:
54+
if should_remove_symlinks:
55+
postfix = [
56+
f"echo Removing python symlinks in {new_dir}/parts/p1/install/bin",
57+
f'rm "{new_dir}/parts/p1/install"/bin/python*',
58+
]
59+
else:
60+
postfix = ['ln -sf "${symlink_target}" "${PARTS_PYTHON_VENV_INTERP_PATH}"']
61+
62+
return [
63+
dedent(
64+
f"""\
65+
# look for a provisioned python interpreter
66+
opts_state="$(set +o|grep errexit)"
67+
set +e
68+
install_dir="{new_dir}/parts/p1/install/usr/bin"
69+
stage_dir="{new_dir}/stage/usr/bin"
70+
71+
# look for the right Python version - if the venv was created with python3.10,
72+
# look for python3.10
73+
basename=$(basename $(readlink -f ${{PARTS_PYTHON_VENV_INTERP_PATH}}))
74+
echo Looking for a Python interpreter called \\"${{basename}}\\" in the payload...
75+
payload_python=$(find "$install_dir" "$stage_dir" -type f -executable -name "${{basename}}" -print -quit 2>/dev/null)
76+
77+
if [ -n "$payload_python" ]; then
78+
# We found a provisioned interpreter, use it.
79+
echo Found interpreter in payload: \\"${{payload_python}}\\"
80+
installed_python="${{payload_python##{new_dir}/parts/p1/install}}"
81+
if [ "$installed_python" = "$payload_python" ]; then
82+
# Found a staged interpreter.
83+
symlink_target="..${{payload_python##{new_dir}/stage}}"
84+
else
85+
# The interpreter was installed but not staged yet.
86+
symlink_target="..$installed_python"
87+
fi
88+
else
89+
# Otherwise use what _get_system_python_interpreter() told us.
90+
echo "Python interpreter not found in payload."
91+
symlink_target="$(readlink -f "$(which "${{PARTS_PYTHON_INTERPRETER}}")")"
92+
fi
93+
94+
if [ -z "$symlink_target" ]; then
95+
echo "No suitable Python interpreter found, giving up."
96+
exit 1
97+
fi
98+
99+
eval "${{opts_state}}"
100+
"""
101+
),
102+
*postfix,
103+
]
104+
105+
106+
def get_python_shebang_rewrite_commands(
107+
expected_shebang: str, install_dir: str
108+
) -> list[str]:
109+
find_cmd = f'find "{install_dir}" -type f -executable -print0'
110+
xargs_cmd = "xargs --no-run-if-empty -0"
111+
sed_cmd = (
112+
f'sed -i "1 s|^#\\!${{PARTS_PYTHON_VENV_INTERP_PATH}}.*$|{expected_shebang}|"'
113+
)
114+
return [
115+
dedent(
116+
f"""\
117+
{find_cmd} | {xargs_cmd} \\
118+
{sed_cmd}
119+
"""
120+
)
121+
]
122+
123+
124+
def test_get_build_packages(plugin) -> None:
125+
assert plugin.get_build_packages() == {"findutils", "python3-venv", "python3-dev"}
126+
127+
128+
def test_get_build_environment(plugin, new_dir) -> None:
129+
assert plugin.get_build_environment() == {
130+
"PATH": f"{new_dir}/parts/p1/install/bin:${{PATH}}",
131+
"PARTS_PYTHON_INTERPRETER": "python3",
132+
"PARTS_PYTHON_VENV_ARGS": "",
133+
}
134+
135+
136+
def test_get_build_commands(plugin, new_dir) -> None:
137+
venv_path = new_dir / "parts" / "p1" / "install"
138+
assert plugin.get_build_commands() == [
139+
f'"${{PARTS_PYTHON_INTERPRETER}}" -m venv ${{PARTS_PYTHON_VENV_ARGS}} "{venv_path}"',
140+
f'PARTS_PYTHON_VENV_INTERP_PATH="{venv_path}/bin/${{PARTS_PYTHON_INTERPRETER}}"',
141+
'"${PARTS_PYTHON_INTERPRETER}" -m fake_pip --install',
142+
*get_python_shebang_rewrite_commands(
143+
"#!/usr/bin/env ${PARTS_PYTHON_INTERPRETER}",
144+
str(plugin._part_info.part_install_dir),
145+
),
146+
*get_python_build_commands(new_dir, should_remove_symlinks=False),
147+
]
148+
149+
150+
def test_call_should_remove_symlinks(plugin, new_dir, monkeypatch):
151+
monkeypatch.setattr(plugin, "_should_remove_symlinks", lambda: True)
152+
153+
venv_path = new_dir / "parts" / "p1" / "install"
154+
assert plugin.get_build_commands() == [
155+
f'"${{PARTS_PYTHON_INTERPRETER}}" -m venv ${{PARTS_PYTHON_VENV_ARGS}} "{venv_path}"',
156+
f'PARTS_PYTHON_VENV_INTERP_PATH="{venv_path}/bin/${{PARTS_PYTHON_INTERPRETER}}"',
157+
'"${PARTS_PYTHON_INTERPRETER}" -m fake_pip --install',
158+
*get_python_shebang_rewrite_commands(
159+
"#!/usr/bin/env ${PARTS_PYTHON_INTERPRETER}",
160+
str(plugin._part_info.part_install_dir),
161+
),
162+
*get_python_build_commands(new_dir, should_remove_symlinks=True),
163+
]
164+
165+
166+
def test_script_interpreter(plugin):
167+
assert plugin._get_script_interpreter() == (
168+
"#!/usr/bin/env ${PARTS_PYTHON_INTERPRETER}"
169+
)
170+
171+
172+
def test_get_system_python_interpreter(plugin):
173+
assert plugin._get_system_python_interpreter() == (
174+
'$(readlink -f "$(which "${PARTS_PYTHON_INTERPRETER}")")'
175+
)

tests/unit/plugins/test_poetry_plugin.py

+8-100
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
# You should have received a copy of the GNU Lesser General Public License
1515
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17-
from pathlib import Path
18-
from textwrap import dedent
19-
2017
import pytest
2118
import pytest_check # type: ignore[import-untyped]
2219
from craft_parts import Part, PartInfo, ProjectInfo
@@ -59,82 +56,6 @@ def test_get_build_packages(
5956
assert added_poetry == expected_added_poetry
6057

6158

62-
def test_get_build_environment(plugin, new_dir):
63-
assert plugin.get_build_environment() == {
64-
"PATH": f"{new_dir}/parts/p1/install/bin:${{PATH}}",
65-
"PARTS_PYTHON_INTERPRETER": "python3",
66-
"PARTS_PYTHON_VENV_ARGS": "",
67-
}
68-
69-
70-
# pylint: disable=line-too-long
71-
72-
73-
def get_build_commands(
74-
new_dir: Path, *, should_remove_symlinks: bool = False
75-
) -> list[str]:
76-
if should_remove_symlinks:
77-
postfix = dedent(
78-
f"""\
79-
echo Removing python symlinks in {new_dir}/parts/p1/install/bin
80-
rm "{new_dir}/parts/p1/install"/bin/python*
81-
"""
82-
)
83-
else:
84-
postfix = dedent(
85-
'ln -sf "${symlink_target}" "${PARTS_PYTHON_VENV_INTERP_PATH}"'
86-
)
87-
88-
return [
89-
dedent(
90-
f"""\
91-
find "{new_dir}/parts/p1/install" -type f -executable -print0 | xargs --no-run-if-empty -0 \\
92-
sed -i "1 s|^#\\!${{PARTS_PYTHON_VENV_INTERP_PATH}}.*$|#!/usr/bin/env ${{PARTS_PYTHON_INTERPRETER}}|"
93-
"""
94-
),
95-
dedent(
96-
f"""\
97-
# look for a provisioned python interpreter
98-
opts_state="$(set +o|grep errexit)"
99-
set +e
100-
install_dir="{new_dir}/parts/p1/install/usr/bin"
101-
stage_dir="{new_dir}/stage/usr/bin"
102-
103-
# look for the right Python version - if the venv was created with python3.10,
104-
# look for python3.10
105-
basename=$(basename $(readlink -f ${{PARTS_PYTHON_VENV_INTERP_PATH}}))
106-
echo Looking for a Python interpreter called \\"${{basename}}\\" in the payload...
107-
payload_python=$(find "$install_dir" "$stage_dir" -type f -executable -name "${{basename}}" -print -quit 2>/dev/null)
108-
109-
if [ -n "$payload_python" ]; then
110-
# We found a provisioned interpreter, use it.
111-
echo Found interpreter in payload: \\"${{payload_python}}\\"
112-
installed_python="${{payload_python##{new_dir}/parts/p1/install}}"
113-
if [ "$installed_python" = "$payload_python" ]; then
114-
# Found a staged interpreter.
115-
symlink_target="..${{payload_python##{new_dir}/stage}}"
116-
else
117-
# The interpreter was installed but not staged yet.
118-
symlink_target="..$installed_python"
119-
fi
120-
else
121-
# Otherwise use what _get_system_python_interpreter() told us.
122-
echo "Python interpreter not found in payload."
123-
symlink_target="$(readlink -f "$(which "${{PARTS_PYTHON_INTERPRETER}}")")"
124-
fi
125-
126-
if [ -z "$symlink_target" ]; then
127-
echo "No suitable Python interpreter found, giving up."
128-
exit 1
129-
fi
130-
131-
eval "${{opts_state}}"
132-
"""
133-
),
134-
postfix,
135-
]
136-
137-
13859
@pytest.mark.parametrize(
13960
("optional_groups", "poetry_extra_args", "export_addendum"),
14061
[
@@ -152,7 +73,7 @@ def get_build_commands(
15273
(["--pre", "-U"], "--pre -U"),
15374
],
15475
)
155-
def test_get_build_commands(
76+
def test_get_install_commands(
15677
new_dir,
15778
optional_groups,
15879
poetry_extra_args,
@@ -173,15 +94,14 @@ def test_get_build_commands(
17394

17495
plugin = PoetryPlugin(part_info=part_info, properties=properties)
17596

176-
assert plugin.get_build_commands() == [
177-
f'"${{PARTS_PYTHON_INTERPRETER}}" -m venv ${{PARTS_PYTHON_VENV_ARGS}} "{new_dir}/parts/p1/install"',
178-
f'PARTS_PYTHON_VENV_INTERP_PATH="{new_dir}/parts/p1/install/bin/${{PARTS_PYTHON_INTERPRETER}}"',
179-
f"poetry export --format=requirements.txt --output={new_dir}/parts/p1/build/requirements.txt --with-credentials"
97+
requirements = new_dir / "parts" / "p1" / "build" / "requirements.txt"
98+
pip = new_dir / "parts" / "p1" / "install" / "bin" / "pip"
99+
assert plugin._get_package_install_commands() == [
100+
f"poetry export --format=requirements.txt --output={requirements} --with-credentials"
180101
+ export_addendum,
181-
f"{new_dir}/parts/p1/install/bin/pip install {pip_addendum} --requirement={new_dir}/parts/p1/build/requirements.txt",
182-
f"{new_dir}/parts/p1/install/bin/pip install --no-deps .",
183-
f"{new_dir}/parts/p1/install/bin/pip check",
184-
*get_build_commands(new_dir),
102+
f"{pip} install {pip_addendum} --requirement={requirements}",
103+
f"{pip} install --no-deps .",
104+
f"{pip} check",
185105
]
186106

187107

@@ -202,18 +122,6 @@ def test_should_remove_symlinks(plugin):
202122
assert plugin._should_remove_symlinks() is False
203123

204124

205-
def test_get_system_python_interpreter(plugin):
206-
assert plugin._get_system_python_interpreter() == (
207-
'$(readlink -f "$(which "${PARTS_PYTHON_INTERPRETER}")")'
208-
)
209-
210-
211-
def test_script_interpreter(plugin):
212-
assert plugin._get_script_interpreter() == (
213-
"#!/usr/bin/env ${PARTS_PYTHON_INTERPRETER}"
214-
)
215-
216-
217125
def test_call_should_remove_symlinks(plugin, new_dir, mocker):
218126
mocker.patch(
219127
"craft_parts.plugins.poetry_plugin.PoetryPlugin._should_remove_symlinks",

0 commit comments

Comments
 (0)