-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: python plugin file listing #943
Changes from 19 commits
18deeb0
9926a0d
9a53d08
e967b15
62c034c
befec8f
deca1a2
d24a5a3
684729c
be6ec70
5db00b1
e17057b
6a8e8ce
344dc5a
a3e158f
1528104
7968c83
a23b436
737c0ca
5fbace5
e2bbdfd
a68e014
031cf84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,10 +19,13 @@ | |
import textwrap | ||
from pathlib import Path | ||
|
||
import craft_parts.plugins.plugins | ||
import pytest | ||
import yaml | ||
from craft_parts import LifecycleManager, Step, errors, plugins | ||
from craft_parts.infos import PartInfo, ProjectInfo | ||
from craft_parts.parts import Part | ||
from craft_parts.plugins.base import Package | ||
from craft_parts.plugins.python_plugin import PythonPlugin | ||
from overrides import override | ||
|
||
|
||
|
@@ -123,7 +126,7 @@ def test_python_plugin_symlink(new_dir, partitions): | |
def test_python_plugin_override_get_system_interpreter(new_dir, partitions): | ||
"""Override the system interpreter, link should use it.""" | ||
|
||
class MyPythonPlugin(craft_parts.plugins.plugins.PythonPlugin): | ||
class MyPythonPlugin(PythonPlugin): | ||
@override | ||
def _get_system_python_interpreter(self) -> str | None: | ||
return "use-this-python" | ||
|
@@ -159,7 +162,7 @@ def test_python_plugin_no_system_interpreter( | |
): | ||
"""Check that the build fails if a payload interpreter is needed but not found.""" | ||
|
||
class MyPythonPlugin(craft_parts.plugins.plugins.PythonPlugin): | ||
class MyPythonPlugin(PythonPlugin): | ||
@override | ||
def _get_system_python_interpreter(self) -> str | None: | ||
return None | ||
|
@@ -194,7 +197,7 @@ def _should_remove_symlinks(self) -> bool: | |
def test_python_plugin_remove_symlinks(new_dir, partitions): | ||
"""Override symlink removal.""" | ||
|
||
class MyPythonPlugin(craft_parts.plugins.plugins.PythonPlugin): | ||
class MyPythonPlugin(PythonPlugin): | ||
@override | ||
def _should_remove_symlinks(self) -> bool: | ||
return True | ||
|
@@ -250,7 +253,7 @@ def test_python_plugin_fix_shebangs(new_dir, partitions): | |
def test_python_plugin_override_shebangs(new_dir, partitions): | ||
"""Override what we want in script shebang lines.""" | ||
|
||
class MyPythonPlugin(craft_parts.plugins.plugins.PythonPlugin): | ||
class MyPythonPlugin(PythonPlugin): | ||
@override | ||
def _get_script_interpreter(self) -> str: | ||
return "#!/my/script/interpreter" | ||
|
@@ -298,7 +301,7 @@ def test_find_payload_python_bad_version(new_dir, partitions): | |
"""Test that the build fails if a payload interpreter is needed but it's the | ||
wrong Python version.""" | ||
|
||
class MyPythonPlugin(craft_parts.plugins.plugins.PythonPlugin): | ||
class MyPythonPlugin(PythonPlugin): | ||
@override | ||
def _get_system_python_interpreter(self) -> str | None: | ||
# To have the build fail after failing to find the payload interpreter | ||
|
@@ -374,7 +377,7 @@ def test_find_payload_python_good_version(new_dir, partitions): | |
def test_no_shebangs(new_dir, partitions): | ||
"""Test that building a Python part with no scripts works.""" | ||
|
||
class ScriptlessPlugin(craft_parts.plugins.plugins.PythonPlugin): | ||
class ScriptlessPlugin(PythonPlugin): | ||
@override | ||
def _get_package_install_commands(self) -> list[str]: | ||
return [ | ||
|
@@ -408,3 +411,78 @@ def _get_package_install_commands(self) -> list[str]: | |
|
||
primed_script = lf.project_info.prime_dir / "bin/mytest" | ||
assert not primed_script.exists() | ||
|
||
|
||
@pytest.mark.slow | ||
def test_python_plugin_get_files(new_dir, partitions): | ||
parts_yaml = textwrap.dedent( | ||
"""\ | ||
parts: | ||
foo: | ||
plugin: python | ||
source: . | ||
python-packages: [flask==3.1.0] | ||
""" | ||
) | ||
parts = yaml.safe_load(parts_yaml) | ||
|
||
lifecycle = LifecycleManager( | ||
parts, | ||
application_name="test_python", | ||
cache_dir=new_dir, | ||
partitions=partitions, | ||
) | ||
actions = lifecycle.plan(Step.BUILD) | ||
|
||
with lifecycle.action_executor() as ctx: | ||
ctx.execute(actions) | ||
|
||
part_name = list(parts["parts"].keys())[0] | ||
actual_file_list = lifecycle._executor._handler[part_name]._plugin.get_files() | ||
lengau marked this conversation as resolved.
Show resolved
Hide resolved
|
||
part_install_dir = lifecycle._executor._part_list[0].part_install_dir | ||
|
||
# Real quick instantiate another copy of the plugin to ensure statelessness. | ||
properties2 = PythonPlugin.properties_class.unmarshal(parts["parts"]["foo"]) | ||
part_info = PartInfo( | ||
project_info=ProjectInfo( | ||
application_name="test", cache_dir=new_dir, partitions=partitions | ||
), | ||
part=Part("foo", {"source": "."}, partitions=partitions), | ||
) | ||
plugin2 = PythonPlugin(properties=properties2, part_info=part_info) | ||
assert plugin2.get_files() == actual_file_list | ||
|
||
# Make sure all the expected packages were installed. | ||
# We can't assert the exact set of keys because the pip version will change | ||
# over time. And we can't assert the number of keys because py3.10 installs | ||
# setuptools as a separate package. | ||
|
||
assert Package(name="Flask", version="3.1.0") in actual_file_list | ||
assert part_install_dir / "bin/flask" in actual_file_list[Package("Flask", "3.1.0")] | ||
|
||
# Can't assert specific versions here because flask has >= versions for its | ||
# dependencies. | ||
seeking_pkgs = { | ||
pkgname: False | ||
for pkgname in [ | ||
"Jinja2", | ||
"Werkzeug", | ||
"blinker", | ||
"click", | ||
"itsdangerous", | ||
] | ||
} | ||
for found_pkg in actual_file_list: | ||
# Check a few specifics to make sure we got package contents correctly | ||
if found_pkg.name == "Jinja2": | ||
assert len(actual_file_list[found_pkg]) == 57 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can't know this for anything where we're not asserting a specific package. We can probably presume it to be a nonzero number, but the only one we can have any real confidence in is flask. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dammit, good point. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
elif found_pkg.name == "Werkzeug": | ||
assert len(actual_file_list[found_pkg]) == 116 | ||
|
||
for sought_pkg in seeking_pkgs: | ||
if found_pkg.name == sought_pkg: | ||
seeking_pkgs[sought_pkg] = True | ||
break | ||
mattculler marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sought_collapsed = set(seeking_pkgs.values()) | ||
success = len(sought_collapsed) == 1 and sought_collapsed.pop() | ||
assert success, f"Didn't find one or more expected packages: {seeking_pkgs}" | ||
mattculler marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
#!/bin/sh | ||
echo it is done |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
This software is covered by the fake license. The terms of this license do not apply to the software. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Name: fakeee | ||
Version: 1.2.3-deb_ian |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
../../../bin/doit,, | ||
fakeee/a_file.py,, | ||
fakeee/things/stuff.py,, | ||
fakeee/things/nothing.py,, | ||
fakeee-1.2.3-deb_ian.dist-info/LICENSE.txt,, | ||
fakeee-1.2.3-deb_ian.dist-info/METADATA,, | ||
fakeee-1.2.3-deb_ian.dist-info/RECORD,, | ||
fakeee-1.2.3-deb_ian.dist-info/REQUESTED,, |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
def func(): | ||
return True |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
def lying_function(): | ||
return "lying_function was not executed" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
def dontdoit(a): | ||
print(f"Not done: {a}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll bet we can put this in the base Python plugin. In all three cases they generate PEP 405 compatible virtual environments in
self._get_venv_directory()
. (I'm fine if you don't do so in this PR, but when we get to the point of implementing it for poetry and uv it probably comes roughly for free)