Commit 2ed53be4 by Sébastien Eustace

Fix root package installation with pip>=19.0

parent 457e2205
...@@ -35,3 +35,4 @@ MANIFEST.in ...@@ -35,3 +35,4 @@ MANIFEST.in
.venv .venv
/releases/* /releases/*
pip-wheel-metadata
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
### Fixed ### Fixed
- Fixed root package installation with `pip>=19.0`.
- Fixed packages not being removed after using the `remove` command. - 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. ...@@ -28,10 +28,8 @@ exist it will look for <comment>pyproject.toml</> and do the same.
def handle(self): def handle(self):
from poetry.installation import Installer from poetry.installation import Installer
from poetry.io import NullIO 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.masonry.utils.module import ModuleOrPackageNotFound
from poetry.utils._compat import decode
from poetry.utils.env import NullEnv
installer = Installer( installer = Installer(
self.output, self.output,
...@@ -60,7 +58,7 @@ exist it will look for <comment>pyproject.toml</> and do the same. ...@@ -60,7 +58,7 @@ exist it will look for <comment>pyproject.toml</> and do the same.
return return_code return return_code
try: try:
builder = SdistBuilder(self.poetry, NullEnv(), NullIO()) builder = EditableBuilder(self.poetry, self._env, NullIO())
except ModuleOrPackageNotFound: except ModuleOrPackageNotFound:
# This is likely due to the fact that the project is an application # This is likely due to the fact that the project is an application
# not following the structure expected by Poetry # not following the structure expected by Poetry
...@@ -76,17 +74,6 @@ exist it will look for <comment>pyproject.toml</> and do the same. ...@@ -76,17 +74,6 @@ exist it will look for <comment>pyproject.toml</> and do the same.
if self.option("dry-run"): if self.option("dry-run"):
return 0 return 0
setup = self.poetry.file.parent / "setup.py" builder.build()
has_setup = setup.exists()
if has_setup: return 0
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))
from .complete import CompleteBuilder from .complete import CompleteBuilder
from .editable import EditableBuilder
from .sdist import SdistBuilder from .sdist import SdistBuilder
from .wheel import WheelBuilder from .wheel import WheelBuilder
...@@ -35,7 +35,7 @@ class Builder(object): ...@@ -35,7 +35,7 @@ class Builder(object):
AVAILABLE_PYTHONS = {"2", "2.7", "3", "3.4", "3.5", "3.6", "3.7"} 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._poetry = poetry
self._env = env self._env = env
self._io = io 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 json
import os import os
import platform import platform
import re
import subprocess import subprocess
import sys import sys
import sysconfig import sysconfig
...@@ -15,6 +16,7 @@ from typing import Tuple ...@@ -15,6 +16,7 @@ from typing import Tuple
from poetry.config import Config from poetry.config import Config
from poetry.locations import CACHE_DIR from poetry.locations import CACHE_DIR
from poetry.semver.version import Version
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils._compat import decode from poetry.utils._compat import decode
from poetry.utils._compat import encode from poetry.utils._compat import encode
...@@ -114,6 +116,7 @@ class Env(object): ...@@ -114,6 +116,7 @@ class Env(object):
self._base = base or path self._base = base or path
self._marker_env = None self._marker_env = None
self._pip_version = None
@property @property
def path(self): # type: () -> Path def path(self): # type: () -> Path
...@@ -152,6 +155,25 @@ class Env(object): ...@@ -152,6 +155,25 @@ class Env(object):
""" """
return self._bin("pip") 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 @classmethod
def get(cls, cwd, reload=False): # type: (Path, bool) -> Env def get(cls, cwd, reload=False): # type: (Path, bool) -> Env
if cls._env is not None and not reload: if cls._env is not None and not reload:
...@@ -308,6 +330,9 @@ class Env(object): ...@@ -308,6 +330,9 @@ class Env(object):
def config_var(self, var): # type: (str) -> Any def config_var(self, var): # type: (str) -> Any
raise NotImplementedError() raise NotImplementedError()
def get_pip_version(self): # type: () -> Version
raise NotImplementedError()
def is_valid_for_marker(self, marker): # type: (BaseMarker) -> bool def is_valid_for_marker(self, marker): # type: (BaseMarker) -> bool
return marker.validate(self.marker_env) return marker.validate(self.marker_env)
...@@ -424,6 +449,11 @@ class SystemEnv(Env): ...@@ -424,6 +449,11 @@ class SystemEnv(Env):
return return
def get_pip_version(self): # type: () -> Version
from pip import __version__
return Version.parse(__version__)
def is_venv(self): # type: () -> bool def is_venv(self): # type: () -> bool
return self._path != self._base return self._path != self._base
...@@ -474,6 +504,14 @@ class VirtualEnv(Env): ...@@ -474,6 +504,14 @@ class VirtualEnv(Env):
return value 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 def is_venv(self): # type: () -> bool
return True return True
...@@ -546,6 +584,7 @@ class MockEnv(NullEnv): ...@@ -546,6 +584,7 @@ class MockEnv(NullEnv):
platform="darwin", platform="darwin",
os_name="posix", os_name="posix",
is_venv=False, is_venv=False,
pip_version="19.1",
**kwargs **kwargs
): ):
super(MockEnv, self).__init__(**kwargs) super(MockEnv, self).__init__(**kwargs)
...@@ -555,6 +594,7 @@ class MockEnv(NullEnv): ...@@ -555,6 +594,7 @@ class MockEnv(NullEnv):
self._platform = platform self._platform = platform
self._os_name = os_name self._os_name = os_name
self._is_venv = is_venv self._is_venv = is_venv
self._pip_version = Version.parse(pip_version)
@property @property
def version_info(self): # type: () -> Tuple[int] def version_info(self): # type: () -> Tuple[int]
...@@ -572,5 +612,9 @@ class MockEnv(NullEnv): ...@@ -572,5 +612,9 @@ class MockEnv(NullEnv):
def os(self): # type: () -> str def os(self): # type: () -> str
return self._os_name return self._os_name
@property
def pip_version(self):
return self._pip_version
def is_venv(self): # type: () -> bool def is_venv(self): # type: () -> bool
return self._is_venv 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 ...@@ -3,15 +3,11 @@ import pytest
import shutil import shutil
import zipfile import zipfile
from email.parser import Parser
from poetry.io import NullIO from poetry.io import NullIO
from poetry.masonry.builders import WheelBuilder from poetry.masonry.builders import WheelBuilder
from poetry.poetry import Poetry from poetry.poetry import Poetry
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils._compat import to_str
from poetry.utils.env import NullEnv from poetry.utils.env import NullEnv
from poetry.packages import ProjectPackage
fixtures_dir = Path(__file__).parent / "fixtures" 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