384 lines
12 KiB
Python
384 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import csv
|
|
import json
|
|
import locale
|
|
import os
|
|
import shutil
|
|
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
from typing import Iterator
|
|
|
|
import pytest
|
|
|
|
from cleo.io.null_io import NullIO
|
|
from deepdiff import DeepDiff
|
|
from poetry.core.constraints.version import Version
|
|
from poetry.core.packages.package import Package
|
|
|
|
from poetry.factory import Factory
|
|
from poetry.masonry.builders.editable import EditableBuilder
|
|
from poetry.repositories.installed_repository import InstalledRepository
|
|
from poetry.utils.env import EnvCommandError
|
|
from poetry.utils.env import EnvManager
|
|
from poetry.utils.env import MockEnv
|
|
from poetry.utils.env import VirtualEnv
|
|
from poetry.utils.env import ephemeral_environment
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from pytest_mock import MockerFixture
|
|
from tests.types import FixtureDirGetter
|
|
|
|
from poetry.poetry import Poetry
|
|
|
|
|
|
@pytest.fixture()
|
|
def simple_poetry(fixture_dir: FixtureDirGetter) -> Poetry:
|
|
poetry = Factory().create_poetry(fixture_dir("simple_project"))
|
|
|
|
return poetry
|
|
|
|
|
|
@pytest.fixture()
|
|
def project_with_include(fixture_dir: FixtureDirGetter) -> Poetry:
|
|
poetry = Factory().create_poetry(fixture_dir("with-include"))
|
|
|
|
return poetry
|
|
|
|
|
|
@pytest.fixture()
|
|
def extended_poetry(fixture_dir: FixtureDirGetter) -> Poetry:
|
|
poetry = Factory().create_poetry(fixture_dir("extended_project"))
|
|
|
|
return poetry
|
|
|
|
|
|
@pytest.fixture()
|
|
def extended_without_setup_poetry(fixture_dir: FixtureDirGetter) -> Poetry:
|
|
poetry = Factory().create_poetry(fixture_dir("extended_project_without_setup"))
|
|
|
|
return poetry
|
|
|
|
|
|
@pytest.fixture
|
|
def with_multiple_readme_files(fixture_dir: FixtureDirGetter) -> Poetry:
|
|
poetry = Factory().create_poetry(fixture_dir("with_multiple_readme_files"))
|
|
|
|
return poetry
|
|
|
|
|
|
@pytest.fixture()
|
|
def env_manager(simple_poetry: Poetry) -> EnvManager:
|
|
return EnvManager(simple_poetry)
|
|
|
|
|
|
@pytest.fixture
|
|
def tmp_venv(tmp_path: Path, env_manager: EnvManager) -> Iterator[VirtualEnv]:
|
|
venv_path = tmp_path / "venv"
|
|
|
|
env_manager.build_venv(venv_path)
|
|
|
|
venv = VirtualEnv(venv_path)
|
|
yield venv
|
|
|
|
shutil.rmtree(str(venv.path))
|
|
|
|
|
|
@pytest.fixture()
|
|
def bad_scripts_no_colon(fixture_dir: FixtureDirGetter) -> Poetry:
|
|
poetry = Factory().create_poetry(fixture_dir("bad_scripts_project/no_colon"))
|
|
|
|
return poetry
|
|
|
|
|
|
@pytest.fixture()
|
|
def bad_scripts_too_many_colon(fixture_dir: FixtureDirGetter) -> Poetry:
|
|
poetry = Factory().create_poetry(fixture_dir("bad_scripts_project/too_many_colon"))
|
|
|
|
return poetry
|
|
|
|
|
|
def test_builder_installs_proper_files_for_standard_packages(
|
|
simple_poetry: Poetry, tmp_venv: VirtualEnv
|
|
) -> None:
|
|
builder = EditableBuilder(simple_poetry, tmp_venv, NullIO())
|
|
|
|
builder.build()
|
|
|
|
assert tmp_venv._bin_dir.joinpath("foo").exists()
|
|
pth_file = Path("simple_project.pth")
|
|
assert tmp_venv.site_packages.exists(pth_file)
|
|
assert (
|
|
simple_poetry.file.path.parent.resolve().as_posix()
|
|
== tmp_venv.site_packages.find(pth_file)[0].read_text().strip(os.linesep)
|
|
)
|
|
|
|
dist_info = Path("simple_project-1.2.3.dist-info")
|
|
assert tmp_venv.site_packages.exists(dist_info)
|
|
|
|
dist_info = tmp_venv.site_packages.find(dist_info)[0]
|
|
|
|
assert dist_info.joinpath("INSTALLER").exists()
|
|
assert dist_info.joinpath("METADATA").exists()
|
|
assert dist_info.joinpath("RECORD").exists()
|
|
assert dist_info.joinpath("entry_points.txt").exists()
|
|
assert dist_info.joinpath("direct_url.json").exists()
|
|
|
|
assert not DeepDiff(
|
|
{
|
|
"dir_info": {"editable": True},
|
|
"url": simple_poetry.file.path.parent.as_uri(),
|
|
},
|
|
json.loads(dist_info.joinpath("direct_url.json").read_text()),
|
|
)
|
|
|
|
assert dist_info.joinpath("INSTALLER").read_text() == "poetry"
|
|
assert (
|
|
dist_info.joinpath("entry_points.txt").read_text()
|
|
== "[console_scripts]\nbaz=bar:baz.boom.bim\nfoo=foo:bar\n"
|
|
"fox=fuz.foo:bar.baz\n\n"
|
|
)
|
|
python_classifiers = "\n".join(
|
|
f"Classifier: Programming Language :: Python :: {version}"
|
|
for version in sorted(
|
|
Package.AVAILABLE_PYTHONS,
|
|
key=lambda x: tuple(map(int, x.split("."))),
|
|
)
|
|
)
|
|
metadata = f"""\
|
|
Metadata-Version: 2.1
|
|
Name: simple-project
|
|
Version: 1.2.3
|
|
Summary: Some description.
|
|
Home-page: https://python-poetry.org
|
|
License: MIT
|
|
Keywords: packaging,dependency,poetry
|
|
Author: Sébastien Eustace
|
|
Author-email: sebastien@eustace.io
|
|
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
|
|
Classifier: License :: OSI Approved :: MIT License
|
|
{python_classifiers}
|
|
Classifier: Topic :: Software Development :: Build Tools
|
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
Project-URL: Documentation, https://python-poetry.org/docs
|
|
Project-URL: Repository, https://github.com/python-poetry/poetry
|
|
Description-Content-Type: text/x-rst
|
|
|
|
My Package
|
|
==========
|
|
|
|
"""
|
|
assert metadata == dist_info.joinpath("METADATA").read_text(encoding="utf-8")
|
|
|
|
with open(dist_info.joinpath("RECORD"), encoding="utf-8", newline="") as f:
|
|
reader = csv.reader(f)
|
|
records = list(reader)
|
|
|
|
assert all(len(row) == 3 for row in records)
|
|
record_entries = {row[0] for row in records}
|
|
pth_file = Path("simple_project.pth")
|
|
assert tmp_venv.site_packages.exists(pth_file)
|
|
assert str(tmp_venv.site_packages.find(pth_file)[0]) in record_entries
|
|
assert str(tmp_venv._bin_dir.joinpath("foo")) in record_entries
|
|
assert str(tmp_venv._bin_dir.joinpath("baz")) in record_entries
|
|
assert str(dist_info.joinpath("METADATA")) in record_entries
|
|
assert str(dist_info.joinpath("INSTALLER")) in record_entries
|
|
assert str(dist_info.joinpath("entry_points.txt")) in record_entries
|
|
assert str(dist_info.joinpath("RECORD")) in record_entries
|
|
assert str(dist_info.joinpath("direct_url.json")) in record_entries
|
|
|
|
baz_script = f"""\
|
|
#!{tmp_venv.python}
|
|
import sys
|
|
from bar import baz
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(baz.boom.bim())
|
|
"""
|
|
|
|
assert baz_script == tmp_venv._bin_dir.joinpath("baz").read_text()
|
|
|
|
foo_script = f"""\
|
|
#!{tmp_venv.python}
|
|
import sys
|
|
from foo import bar
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(bar())
|
|
"""
|
|
|
|
assert foo_script == tmp_venv._bin_dir.joinpath("foo").read_text()
|
|
|
|
fox_script = f"""\
|
|
#!{tmp_venv.python}
|
|
import sys
|
|
from fuz.foo import bar
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(bar.baz())
|
|
"""
|
|
|
|
assert fox_script == tmp_venv._bin_dir.joinpath("fox").read_text()
|
|
|
|
|
|
def test_builder_falls_back_on_setup_and_pip_for_packages_with_build_scripts(
|
|
mocker: MockerFixture, extended_poetry: Poetry, tmp_path: Path
|
|
) -> None:
|
|
pip_install = mocker.patch("poetry.masonry.builders.editable.pip_install")
|
|
env = MockEnv(path=tmp_path / "foo")
|
|
builder = EditableBuilder(extended_poetry, env, NullIO())
|
|
|
|
builder.build()
|
|
pip_install.assert_called_once_with(
|
|
extended_poetry.pyproject.file.path.parent, env, upgrade=True, editable=True
|
|
)
|
|
assert [] == env.executed
|
|
|
|
|
|
@pytest.mark.network
|
|
def test_builder_setup_generation_runs_with_pip_editable(
|
|
fixture_dir: FixtureDirGetter, tmp_path: Path
|
|
) -> None:
|
|
# create an isolated copy of the project
|
|
fixture = fixture_dir("extended_project")
|
|
extended_project = tmp_path / "extended_project"
|
|
|
|
shutil.copytree(fixture, extended_project)
|
|
assert extended_project.exists()
|
|
|
|
poetry = Factory().create_poetry(extended_project)
|
|
|
|
# we need a venv with pip and setuptools since we are verifying setup.py builds
|
|
with ephemeral_environment(flags={"no-setuptools": False, "no-pip": False}) as venv:
|
|
builder = EditableBuilder(poetry, venv, NullIO())
|
|
builder.build()
|
|
|
|
# is the package installed?
|
|
repository = InstalledRepository.load(venv)
|
|
package = repository.package("extended-project", Version.parse("1.2.3"))
|
|
assert package.name == "extended-project"
|
|
|
|
# check for the module built by build.py
|
|
try:
|
|
output = venv.run_python_script(
|
|
"from extended_project import built; print(built.__file__)"
|
|
).strip()
|
|
except EnvCommandError:
|
|
pytest.fail("Unable to import built module")
|
|
else:
|
|
built_py = Path(output).resolve()
|
|
|
|
expected = extended_project / "extended_project" / "built.py"
|
|
|
|
# ensure the package was installed as editable
|
|
assert built_py == expected.resolve()
|
|
|
|
|
|
def test_builder_installs_proper_files_when_packages_configured(
|
|
project_with_include: Poetry, tmp_venv: VirtualEnv
|
|
) -> None:
|
|
builder = EditableBuilder(project_with_include, tmp_venv, NullIO())
|
|
builder.build()
|
|
|
|
pth_file = Path("with_include.pth")
|
|
assert tmp_venv.site_packages.exists(pth_file)
|
|
|
|
pth_file = tmp_venv.site_packages.find(pth_file)[0]
|
|
|
|
paths = set()
|
|
with pth_file.open(encoding=locale.getpreferredencoding()) as f:
|
|
for line in f.readlines():
|
|
line = line.strip(os.linesep)
|
|
if line:
|
|
paths.add(line)
|
|
|
|
project_root = project_with_include.file.path.parent.resolve()
|
|
expected = {project_root.as_posix(), project_root.joinpath("src").as_posix()}
|
|
|
|
assert paths.issubset(expected)
|
|
assert len(paths) == len(expected)
|
|
|
|
|
|
def test_builder_generates_proper_metadata_when_multiple_readme_files(
|
|
with_multiple_readme_files: Poetry, tmp_venv: VirtualEnv
|
|
) -> None:
|
|
builder = EditableBuilder(with_multiple_readme_files, tmp_venv, NullIO())
|
|
|
|
builder.build()
|
|
|
|
dist_info = Path("my_package-0.1.dist-info")
|
|
assert tmp_venv.site_packages.exists(dist_info)
|
|
|
|
dist_info = tmp_venv.site_packages.find(dist_info)[0]
|
|
assert dist_info.joinpath("METADATA").exists()
|
|
|
|
metadata = """\
|
|
Metadata-Version: 2.1
|
|
Name: my-package
|
|
Version: 0.1
|
|
Summary: Some description.
|
|
Home-page: https://python-poetry.org
|
|
License: MIT
|
|
Author: Your Name
|
|
Author-email: you@example.com
|
|
Requires-Python: >=2.7,<3.0
|
|
Classifier: License :: OSI Approved :: MIT License
|
|
Classifier: Programming Language :: Python :: 2
|
|
Classifier: Programming Language :: Python :: 2.7
|
|
Description-Content-Type: text/x-rst
|
|
|
|
Single Python
|
|
=============
|
|
|
|
Changelog
|
|
=========
|
|
|
|
"""
|
|
assert dist_info.joinpath("METADATA").read_text(encoding="utf-8") == metadata
|
|
|
|
|
|
def test_builder_should_execute_build_scripts(
|
|
mocker: MockerFixture, extended_without_setup_poetry: Poetry, tmp_path: Path
|
|
) -> None:
|
|
env = MockEnv(path=tmp_path / "foo")
|
|
mocker.patch(
|
|
"poetry.masonry.builders.editable.build_environment"
|
|
).return_value.__enter__.return_value = env
|
|
|
|
builder = EditableBuilder(extended_without_setup_poetry, env, NullIO())
|
|
|
|
builder.build()
|
|
|
|
assert [
|
|
["python", str(extended_without_setup_poetry.file.path.parent / "build.py")]
|
|
] == env.executed
|
|
|
|
|
|
def test_builder_catches_bad_scripts_no_colon(
|
|
bad_scripts_no_colon: Poetry, tmp_venv: VirtualEnv
|
|
) -> None:
|
|
builder = EditableBuilder(bad_scripts_no_colon, tmp_venv, NullIO())
|
|
with pytest.raises(ValueError, match=r"Bad script.*") as e:
|
|
builder.build()
|
|
msg = str(e.value)
|
|
# We should print out the problematic script entry
|
|
assert "bar.bin.foo" in msg
|
|
# and some hint about what to do
|
|
assert "Hint:" in msg
|
|
assert 'foo = "bar.bin.foo:main"' in msg
|
|
|
|
|
|
def test_builder_catches_bad_scripts_too_many_colon(
|
|
bad_scripts_too_many_colon: Poetry, tmp_venv: VirtualEnv
|
|
) -> None:
|
|
builder = EditableBuilder(bad_scripts_too_many_colon, tmp_venv, NullIO())
|
|
with pytest.raises(ValueError, match=r"Bad script.*") as e:
|
|
builder.build()
|
|
msg = str(e.value)
|
|
# We should print out the problematic script entry
|
|
assert "foo::bar" in msg
|
|
# and some hint about what is wrong
|
|
assert "Too many" in msg
|