Commit 2ed53be4 by Sébastien Eustace

Fix root package installation with pip>=19.0

parent 457e2205
......@@ -35,3 +35,4 @@ MANIFEST.in
.venv
/releases/*
pip-wheel-metadata
......@@ -4,6 +4,7 @@
### Fixed
- Fixed root package installation with `pip>=19.0`.
- Fixed packages not being removed after using the `remove` command.
......
......@@ -28,10 +28,8 @@ exist it will look for <comment>pyproject.toml</> and do the same.
def handle(self):
from poetry.installation import Installer
from poetry.io import NullIO
from poetry.masonry.builders import SdistBuilder
from poetry.masonry.builders import EditableBuilder
from poetry.masonry.utils.module import ModuleOrPackageNotFound
from poetry.utils._compat import decode
from poetry.utils.env import NullEnv
installer = Installer(
self.output,
......@@ -60,7 +58,7 @@ exist it will look for <comment>pyproject.toml</> and do the same.
return return_code
try:
builder = SdistBuilder(self.poetry, NullEnv(), NullIO())
builder = EditableBuilder(self.poetry, self._env, NullIO())
except ModuleOrPackageNotFound:
# This is likely due to the fact that the project is an application
# not following the structure expected by Poetry
......@@ -76,17 +74,6 @@ exist it will look for <comment>pyproject.toml</> and do the same.
if self.option("dry-run"):
return 0
setup = self.poetry.file.parent / "setup.py"
has_setup = setup.exists()
builder.build()
if has_setup:
self.line("<warning>A setup.py file already exists. Using it.</warning>")
else:
with setup.open("w", encoding="utf-8") as f:
f.write(decode(builder.build_setup()))
try:
self.env.run("pip", "install", "-e", str(setup.parent), "--no-deps")
finally:
if not has_setup:
os.remove(str(setup))
return 0
from .complete import CompleteBuilder
from .editable import EditableBuilder
from .sdist import SdistBuilder
from .wheel import WheelBuilder
......@@ -35,7 +35,7 @@ class Builder(object):
AVAILABLE_PYTHONS = {"2", "2.7", "3", "3.4", "3.5", "3.6", "3.7"}
def __init__(self, poetry, env, io):
def __init__(self, poetry, env, io): # type: (Poetry, Env, IO) -> None
self._poetry = poetry
self._env = env
self._io = io
......
from __future__ import unicode_literals
import os
import shutil
from collections import defaultdict
from poetry.semver.version import Version
from poetry.utils._compat import decode
from .builder import Builder
from .sdist import SdistBuilder
class EditableBuilder(Builder):
def build(self):
if self._package.build:
# If the project has some kind of special
# build needs we delegate to the setup.py file
return self._setup_build()
self._build_egg_info()
self._build_egg_link()
self._add_easy_install_entry()
def _setup_build(self):
builder = SdistBuilder(self._poetry, self._env, self._io)
setup = self._path / "setup.py"
has_setup = setup.exists()
if has_setup:
self._io.write_line(
"<warning>A setup.py file already exists. Using it.</warning>"
)
else:
with setup.open("w", encoding="utf-8") as f:
f.write(decode(builder.build_setup()))
try:
if self._env.pip_version < Version(19, 0):
self._env.run("python", "-m", "pip", "install", "-e", str(self._path))
else:
# Temporarily rename pyproject.toml
shutil.move(
str(self._poetry.file), str(self._poetry.file.with_suffix(".tmp"))
)
try:
self._env.run(
"python", "-m", "pip", "install", "-e", str(self._path)
)
finally:
shutil.move(
str(self._poetry.file.with_suffix(".tmp")),
str(self._poetry.file),
)
finally:
if not has_setup:
os.remove(str(setup))
def _build_egg_info(self):
egg_info = self._path / "{}.egg-info".format(
self._package.name.replace("-", "_")
)
egg_info.mkdir(exist_ok=True)
with egg_info.joinpath("PKG-INFO").open("w", encoding="utf-8") as f:
f.write(decode(self.get_metadata_content()))
with egg_info.joinpath("entry_points.txt").open("w", encoding="utf-8") as f:
entry_points = self.convert_entry_points()
for group_name in sorted(entry_points):
f.write("[{}]\n".format(group_name))
for ep in sorted(entry_points[group_name]):
f.write(ep.replace(" ", "") + "\n")
f.write("\n")
with egg_info.joinpath("requires.txt").open("w", encoding="utf-8") as f:
f.write(self._generate_requires())
def _build_egg_link(self):
egg_link = self._env.site_packages / "{}.egg-link".format(self._package.name)
with egg_link.open("w", encoding="utf-8") as f:
f.write(str(self._poetry.file.parent.resolve()) + "\n")
f.write(".")
def _add_easy_install_entry(self):
easy_install_pth = self._env.site_packages / "easy-install.pth"
path = str(self._poetry.file.parent.resolve())
content = ""
if easy_install_pth.exists():
with easy_install_pth.open(encoding="utf-8") as f:
content = f.read()
if path in content:
return
content += "{}\n".format(path)
with easy_install_pth.open("w", encoding="utf-8") as f:
f.write(content)
def _generate_requires(self):
extras = defaultdict(list)
requires = ""
for dep in sorted(self._package.requires, key=lambda d: d.name):
marker = dep.marker
if marker.is_any():
requires += "{}\n".format(dep.base_pep_508_name)
continue
extras[str(marker)].append(dep.base_pep_508_name)
if extras:
requires += "\n"
for marker, deps in sorted(extras.items()):
requires += "[:{}]\n".format(marker)
for dep in deps:
requires += dep + "\n"
requires += "\n"
return requires
import json
import os
import platform
import re
import subprocess
import sys
import sysconfig
......@@ -15,6 +16,7 @@ from typing import Tuple
from poetry.config import Config
from poetry.locations import CACHE_DIR
from poetry.semver.version import Version
from poetry.utils._compat import Path
from poetry.utils._compat import decode
from poetry.utils._compat import encode
......@@ -114,6 +116,7 @@ class Env(object):
self._base = base or path
self._marker_env = None
self._pip_version = None
@property
def path(self): # type: () -> Path
......@@ -152,6 +155,25 @@ class Env(object):
"""
return self._bin("pip")
@property
def pip_version(self):
if self._pip_version is None:
self._pip_version = self.get_pip_version()
return self._pip_version
@property
def site_packages(self): # type: () -> Path
if self._is_windows:
return self._path / "Lib" / "site-packages"
return (
self._path
/ "lib"
/ "python{}.{}".format(*self.version_info[:2])
/ "site-packages"
)
@classmethod
def get(cls, cwd, reload=False): # type: (Path, bool) -> Env
if cls._env is not None and not reload:
......@@ -308,6 +330,9 @@ class Env(object):
def config_var(self, var): # type: (str) -> Any
raise NotImplementedError()
def get_pip_version(self): # type: () -> Version
raise NotImplementedError()
def is_valid_for_marker(self, marker): # type: (BaseMarker) -> bool
return marker.validate(self.marker_env)
......@@ -424,6 +449,11 @@ class SystemEnv(Env):
return
def get_pip_version(self): # type: () -> Version
from pip import __version__
return Version.parse(__version__)
def is_venv(self): # type: () -> bool
return self._path != self._base
......@@ -474,6 +504,14 @@ class VirtualEnv(Env):
return value
def get_pip_version(self): # type: () -> Version
output = self.run("python", "-m", "pip", "--version").strip()
m = re.match("pip (.+?)(?: from .+)?$", output)
if not m:
return Version.parse("0.0")
return Version.parse(m.group(1))
def is_venv(self): # type: () -> bool
return True
......@@ -546,6 +584,7 @@ class MockEnv(NullEnv):
platform="darwin",
os_name="posix",
is_venv=False,
pip_version="19.1",
**kwargs
):
super(MockEnv, self).__init__(**kwargs)
......@@ -555,6 +594,7 @@ class MockEnv(NullEnv):
self._platform = platform
self._os_name = os_name
self._is_venv = is_venv
self._pip_version = Version.parse(pip_version)
@property
def version_info(self): # type: () -> Tuple[int]
......@@ -572,5 +612,9 @@ class MockEnv(NullEnv):
def os(self): # type: () -> str
return self._os_name
@property
def pip_version(self):
return self._pip_version
def is_venv(self): # type: () -> bool
return self._is_venv
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from poetry.io import NullIO
from poetry.masonry.builders import EditableBuilder
from poetry.poetry import Poetry
from poetry.utils._compat import Path
from poetry.utils.env import MockEnv
fixtures_dir = Path(__file__).parent / "fixtures"
def test_build_pure_python_package(tmp_dir):
tmp_dir = Path(tmp_dir)
env = MockEnv(path=tmp_dir)
env.site_packages.mkdir(parents=True)
module_path = fixtures_dir / "complete"
builder = EditableBuilder(Poetry.create(module_path), env, NullIO())
builder._path = tmp_dir
builder.build()
egg_info = tmp_dir / "my_package.egg-info"
assert egg_info.exists()
entry_points = """\
[console_scripts]
extra-script=my_package.extra:main[time]
my-2nd-script=my_package:main2
my-script=my_package:main
"""
pkg_info = """\
Metadata-Version: 2.1
Name: my-package
Version: 1.2.3
Summary: Some description.
Home-page: https://poetry.eustace.io/
License: MIT
Keywords: packaging,dependency,poetry
Author: Sébastien Eustace
Author-email: sebastien@eustace.io
Requires-Python: >=3.6,<4.0
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Provides-Extra: time
Requires-Dist: cachy[msgpack] (>=0.2.0,<0.3.0)
Requires-Dist: cleo (>=0.6,<0.7)
Requires-Dist: pendulum (>=1.4,<2.0); extra == "time"
Project-URL: Documentation, https://poetry.eustace.io/docs
Project-URL: Repository, https://github.com/sdispater/poetry
Description-Content-Type: text/x-rst
My Package
==========
"""
requires = """\
cachy[msgpack] (>=0.2.0,<0.3.0)
cleo (>=0.6,<0.7)
pendulum (>=1.4,<2.0)
"""
with egg_info.joinpath("entry_points.txt").open(encoding="utf-8") as f:
assert entry_points == f.read()
with egg_info.joinpath("PKG-INFO").open(encoding="utf-8") as f:
assert pkg_info == f.read()
with egg_info.joinpath("requires.txt").open(encoding="utf-8") as f:
assert requires == f.read()
egg_link = env.site_packages / "my-package.egg-link"
with egg_link.open(encoding="utf-8") as f:
assert str(module_path) + "\n." == f.read()
easy_install = env.site_packages / "easy-install.pth"
with easy_install.open(encoding="utf-8") as f:
assert str(module_path) + "\n" in f.readlines()
def test_build_should_delegate_to_pip_for_non_pure_python_packages(tmp_dir, mocker):
move = mocker.patch("shutil.move")
tmp_dir = Path(tmp_dir)
env = MockEnv(path=tmp_dir, pip_version="18.1", execute=False)
env.site_packages.mkdir(parents=True)
module_path = fixtures_dir / "extended"
builder = EditableBuilder(Poetry.create(module_path), env, NullIO())
builder.build()
expected = [["python", "-m", "pip", "install", "-e", str(module_path)]]
assert expected == env.executed
assert 0 == move.call_count
def test_build_should_temporarily_remove_the_pyproject_file(tmp_dir, mocker):
move = mocker.patch("shutil.move")
tmp_dir = Path(tmp_dir)
env = MockEnv(path=tmp_dir, pip_version="19.1", execute=False)
env.site_packages.mkdir(parents=True)
module_path = fixtures_dir / "extended"
builder = EditableBuilder(Poetry.create(module_path), env, NullIO())
builder.build()
expected = [["python", "-m", "pip", "install", "-e", str(module_path)]]
assert expected == env.executed
assert 2 == move.call_count
expected_calls = [
mocker.call(
str(module_path / "pyproject.toml"), str(module_path / "pyproject.tmp")
),
mocker.call(
str(module_path / "pyproject.tmp"), str(module_path / "pyproject.toml")
),
]
assert expected_calls == move.call_args_list
......@@ -3,15 +3,11 @@ import pytest
import shutil
import zipfile
from email.parser import Parser
from poetry.io import NullIO
from poetry.masonry.builders import WheelBuilder
from poetry.poetry import Poetry
from poetry.utils._compat import Path
from poetry.utils._compat import to_str
from poetry.utils.env import NullEnv
from poetry.packages import ProjectPackage
fixtures_dir = Path(__file__).parent / "fixtures"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment