Skip to content

Commit

Permalink
Fix editable install and uninstall
Browse files Browse the repository at this point in the history
  • Loading branch information
frostming committed Jan 22, 2020
1 parent d762adc commit 153efa7
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 55 deletions.
19 changes: 10 additions & 9 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pdm/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.3"
__version__ = "0.0.4"
33 changes: 33 additions & 0 deletions pdm/_editable_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from setuptools.command import easy_install
import sys
import os
import tokenize


def install(setup_py, prefix):
__file__ = setup_py
bin_dir = "Scripts" if os.name == "nt" else "bin"
install_dir = os.path.join(prefix, "lib")
scripts_dir = os.path.join(prefix, bin_dir)

with getattr(tokenize, "open", open)(setup_py) as f:
code = f.read().replace("\\r\\n", "\n")
sys.argv[1:] = [
"develop",
f"--install-dir={install_dir}",
"--no-deps",
f"--prefix={prefix}",
f"--script-dir={scripts_dir}",
f"--site-dirs={install_dir}",
]
# Patches the script writer to inject library path
easy_install.ScriptWriter.template = easy_install.ScriptWriter.template.replace(
"import sys",
"import sys\nsys.path.insert(0, {!r})".format(install_dir.replace("\\", "/")),
)
exec(compile(code, __file__, "exec"))


if __name__ == "__main__":
setup_py, prefix = sys.argv[1:3]
install(setup_py, prefix)
3 changes: 1 addition & 2 deletions pdm/cli/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,7 @@ def do_remove(

def do_list(project: Project) -> None:
working_set = project.environment.get_working_set()
for key in working_set:
dist = working_set[key][0]
for key, dist in working_set.items():
context.io.echo(format_dist(dist))


Expand Down
34 changes: 19 additions & 15 deletions pdm/installers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import importlib
import subprocess
from typing import Dict, List, Tuple

Expand All @@ -11,7 +12,7 @@
from pdm.context import context
from pdm.models.candidates import Candidate
from pdm.models.environment import Environment
from pdm.models.requirements import strip_extras
from pdm.models.requirements import strip_extras, parse_requirement

SETUPTOOLS_SHIM = (
"import setuptools, tokenize;__file__=%r;"
Expand Down Expand Up @@ -69,34 +70,38 @@ def install_wheel(self, wheel: Wheel) -> None:
scripts.executable = self.environment.python_executable
scripts.script_template = scripts.script_template.replace(
"import sys",
"import sys; sys.path.insert(0, {!r})".format(paths["platlib"]),
"import sys\nsys.path.insert(0, {!r})".format(paths["platlib"]),
)
wheel.install(paths, scripts)

def install_editable(self, ireq: shims.InstallRequirement) -> None:
setup_path = ireq.setup_py_path
paths = self.environment.get_paths()
install_script = importlib.import_module(
"pdm._editable_install"
).__file__.strip("co")
install_args = [
self.environment.python_executable,
"-u",
"-c",
SETUPTOOLS_SHIM % setup_path,
"develop",
"--install-dir={}".format(paths["platlib"]),
"--no-deps",
"--prefix={}".format(paths["prefix"]),
"--script-dir={}".format(paths["scripts"]),
"--site-dirs={}".format(paths["platlib"]),
install_script,
setup_path,
paths["prefix"],
]
with self.environment.activate(), cd(ireq.unpacked_source_directory):
subprocess.check_call(install_args)

def uninstall(self, name: str) -> None:
working_set = self.environment.get_working_set()
ireq = shims.install_req_from_line(name)
dist = working_set[name]
req = parse_requirement(name)
if _is_dist_editable(dist):
ireq = shims.install_req_from_editable(dist.location)
else:
ireq = shims.install_req_from_line(name)
ireq.req = req
context.io.echo(
f"Uninstalling: {context.io.green(name, bold=True)} "
f"{context.io.yellow(working_set.by_key[name].version)}"
f"{context.io.yellow(working_set[name].version)}"
)

with self.environment.activate():
Expand All @@ -123,12 +128,11 @@ def compare_with_working_set(self) -> Tuple[List[str], List[str], List[str]]:
to_update, to_remove = [], []
candidates = self.candidates.copy()
environment = self.environment.marker_environment()
for key in working_set:
for key, dist in working_set.items():
if key not in candidates:
to_remove.append(key)
else:
can = candidates.pop(key)
dist = working_set[key][0]
if can.marker and not can.marker.evaluate(environment):
to_remove.append(key)
elif not _is_dist_editable(dist) and dist.version != can.version:
Expand All @@ -139,7 +143,7 @@ def compare_with_working_set(self) -> Tuple[List[str], List[str], List[str]]:
strip_extras(name)[0]
for name, can in candidates.items()
if not (can.marker and not can.marker.evaluate(environment))
and not working_set[strip_extras(name)[0]]
and strip_extras(name)[0] not in working_set
}
)
return to_add, to_update, to_remove
Expand Down
57 changes: 48 additions & 9 deletions pdm/models/environment.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
from __future__ import annotations

import collections
import os
import sys
import sysconfig
from contextlib import contextmanager
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Iterator

from pip._internal.req import req_uninstall
from pip._internal.utils import misc
from pip._vendor import pkg_resources
from pip_shims import shims
from pythonfinder import Finder
from vistir.contextmanagers import temp_environ
from vistir.path import normalize_path

from pdm.context import context
from pdm.exceptions import NoPythonVersion
Expand All @@ -23,16 +27,48 @@
get_python_version,
get_pep508_environment,
)
from pythonfinder import Finder
from vistir.contextmanagers import temp_environ
from vistir.path import normalize_path

if TYPE_CHECKING:
from pdm.models.specifiers import PySpecSet
from pdm.project.config import Config
from pdm._types import Source


class WorkingSet(collections.abc.Mapping):
"""A dict-like class that holds all installed packages in the lib directory."""

def __init__(
self,
paths: Optional[List[str]] = None,
python: Tuple[int, ...] = sys.version_info[:3],
):
self.env = pkg_resources.Environment(paths, python=python)
self.pkg_ws = pkg_resources.WorkingSet(paths)
self.__add_editable_dists()

def __getitem__(self, key: str) -> pkg_resources.Distribution:
rv = self.env[key]
if rv:
return rv[0]
else:
raise KeyError(key)

def __len__(self) -> int:
return len(self.env._distmap)

def __iter__(self) -> Iterator[str]:
for item in self.env:
yield item

def __add_editable_dists(self):
"""Editable distributions are not present in pkg_resources.WorkingSet,
Get them from self.env
"""
missing_keys = [key for key in self if key not in self.pkg_ws.by_key]
for key in missing_keys:
self.pkg_ws.add(self[key])


class Environment:
def __init__(self, python_requires: PySpecSet, config: Config) -> None:
self.python_requires = python_requires
Expand Down Expand Up @@ -78,24 +114,27 @@ def activate(self):
with temp_environ():
old_paths = os.getenv("PYTHONPATH")
if old_paths:
new_paths = os.pathsep.join([paths["platlib"], old_paths])
new_paths = os.pathsep.join([paths["purelib"], old_paths])
else:
new_paths = paths["platlib"]
new_paths = paths["purelib"]
os.environ["PYTHONPATH"] = new_paths
python_root = os.path.dirname(self.python_executable)
os.environ["PATH"] = os.pathsep.join(
[python_root, paths["scripts"], os.environ["PATH"]]
)
working_set = self.get_working_set()
_old_ws = pkg_resources.working_set
pkg_resources.working_set = working_set
pkg_resources.working_set = working_set.pkg_ws
# HACK: Replace the is_local with environment version so that packages can
# be removed correctly.
_old_sitepackages = misc.site_packages
_is_local = misc.is_local
misc.is_local = req_uninstall.is_local = self.is_local
misc.site_packages = paths["purelib"]
yield
misc.is_local = req_uninstall.is_local = _is_local
pkg_resources.working_set = _old_ws
misc.site_packages = _old_sitepackages

def is_local(self, path) -> bool:
return normalize_path(path).startswith(
Expand Down Expand Up @@ -199,10 +238,10 @@ def build(
with builder_class(ireq) as builder:
return builder.build(**kwargs)

def get_working_set(self) -> pkg_resources.Environment:
def get_working_set(self) -> WorkingSet:
"""Get the working set based on local packages directory."""
paths = self.get_paths()
return pkg_resources.Environment(
return WorkingSet(
[paths["platlib"]], python=get_python_version(self.python_executable)
)

Expand Down
21 changes: 11 additions & 10 deletions pdm/models/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@
VCS_REQ = re.compile(
rf"(?P<vcs>{'|'.join(VCS_SCHEMA)})\+" r"(?P<url>[^\s;]+)(?P<marker>[\t ]*;[^\n]+)?"
)
_PATH_START = r"(?:\.|/|[a-zA-Z]:[/\\])"
FILE_REQ = re.compile(
r"(?:(?P<url>\S+://[^\s;]+)|"
rf"(?P<path>{_PATH_START}(?:[^\s;]|\\ )*"
rf"|'{_PATH_START}(?:[^']|\\')*'"
rf"|\"{_PATH_START}(?:[^\"]|\\\")*\"))"
rf"(?P<path>(?:[^\s;]|\\ )*"
rf"|'(?:[^']|\\')*'"
rf"|\"(?:[^\"]|\\\")*\"))"
r"(?P<marker>[\t ]*;[^\n]+)?"
)

Expand Down Expand Up @@ -107,6 +106,9 @@ def __getattr__(self, attr: str) -> Any:
def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self.name}>"

def __str__(self) -> str:
return self.as_line()

@classmethod
def from_req_dict(cls, name: str, req_dict: RequirementDict) -> "Requirement":
# TODO: validate req_dict
Expand Down Expand Up @@ -381,19 +383,18 @@ def filter_requirements_with_extras(

def parse_requirement(line: str, editable: bool = False) -> Requirement:

r = None
m = VCS_REQ.match(line)
if m is not None:
r = VcsRequirement.parse(line, m.groupdict())
else:
m = FILE_REQ.match(line)
if m is not None:
r = FileRequirement.parse(line, m.groupdict())
if r is None:
try:
r = NamedRequirement.parse(line) # type: Requirement
except RequirementParseError as e:
raise RequirementError(str(e)) from None
m = FILE_REQ.match(line)
if m is not None:
r = FileRequirement.parse(line, m.groupdict())
else:
raise RequirementError(str(e)) from None
else:
if r.url:
r = FileRequirement(name=r.name, url=r.url, extras=r.extras)
Expand Down
Loading

0 comments on commit 153efa7

Please sign in to comment.