Skip to content

Commit 8a3d2d7

Browse files
authored
Improve detection of installed packages (#1786)
1 parent fbcea50 commit 8a3d2d7

File tree

5 files changed

+130
-60
lines changed

5 files changed

+130
-60
lines changed

poetry/installation/pip_installer.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,12 @@ def install(self, package, update=False):
9696

9797
self.run(*args)
9898

99-
def update(self, _, target):
99+
def update(self, package, target):
100+
if package.source_type != target.source_type:
101+
# If the source type has changed, we remove the current
102+
# package to avoid perpetual updates in some cases
103+
self.remove(package)
104+
100105
self.install(target, update=True)
101106

102107
def remove(self, package):

poetry/repositories/installed_repository.py

+46-39
Original file line numberDiff line numberDiff line change
@@ -15,45 +15,52 @@ def load(cls, env): # type: (Env) -> InstalledRepository
1515
For now, it uses the pip "freeze" command.
1616
"""
1717
repo = cls()
18+
seen = set()
1819

19-
for distribution in sorted(
20-
metadata.distributions(path=env.sys_path), key=lambda d: str(d._path),
21-
):
22-
name = distribution.metadata["name"]
23-
version = distribution.metadata["version"]
24-
package = Package(name, version, version)
25-
package.description = distribution.metadata.get("summary", "")
26-
27-
repo.add_package(package)
28-
29-
path = Path(str(distribution._path))
30-
is_standard_package = True
31-
try:
32-
path.relative_to(env.site_packages)
33-
except ValueError:
34-
is_standard_package = False
35-
36-
if is_standard_package:
37-
continue
38-
39-
src_path = env.path / "src"
40-
41-
# A VCS dependency should have been installed
42-
# in the src directory. If not, it's a path dependency
43-
try:
44-
path.relative_to(src_path)
45-
46-
from poetry.vcs.git import Git
47-
48-
git = Git()
49-
revision = git.rev_parse("HEAD", src_path / package.name).strip()
50-
url = git.remote_url(src_path / package.name)
51-
52-
package.source_type = "git"
53-
package.source_url = url
54-
package.source_reference = revision
55-
except ValueError:
56-
package.source_type = "directory"
57-
package.source_url = str(path.parent)
20+
for entry in env.sys_path:
21+
for distribution in sorted(
22+
metadata.distributions(path=[entry]), key=lambda d: str(d._path),
23+
):
24+
name = distribution.metadata["name"]
25+
version = distribution.metadata["version"]
26+
package = Package(name, version, version)
27+
package.description = distribution.metadata.get("summary", "")
28+
29+
if package.name in seen:
30+
continue
31+
32+
seen.add(package.name)
33+
34+
repo.add_package(package)
35+
36+
path = Path(str(distribution._path))
37+
is_standard_package = True
38+
try:
39+
path.relative_to(env.site_packages)
40+
except ValueError:
41+
is_standard_package = False
42+
43+
if is_standard_package:
44+
continue
45+
46+
src_path = env.path / "src"
47+
48+
# A VCS dependency should have been installed
49+
# in the src directory. If not, it's a path dependency
50+
try:
51+
path.relative_to(src_path)
52+
53+
from poetry.vcs.git import Git
54+
55+
git = Git()
56+
revision = git.rev_parse("HEAD", src_path / package.name).strip()
57+
url = git.remote_url(src_path / package.name)
58+
59+
package.source_type = "git"
60+
package.source_url = url
61+
package.source_reference = revision
62+
except ValueError:
63+
package.source_type = "directory"
64+
package.source_url = str(path.parent)
5865

5966
return repo

poetry/utils/env.py

+29-14
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,7 @@ def __init__(self, path, base=None): # type: (Path, Optional[Path]) -> None
705705

706706
self._marker_env = None
707707
self._pip_version = None
708+
self._site_packages = None
708709

709710
@property
710711
def path(self): # type: () -> Path
@@ -760,20 +761,25 @@ def pip_version(self):
760761

761762
@property
762763
def site_packages(self): # type: () -> Path
763-
# It seems that PyPy3 virtual environments
764-
# have their site-packages directory at the root
765-
if self._path.joinpath("site-packages").exists():
766-
return self._path.joinpath("site-packages")
767-
768-
if self._is_windows:
769-
return self._path / "Lib" / "site-packages"
770-
771-
return (
772-
self._path
773-
/ "lib"
774-
/ "python{}.{}".format(*self.version_info[:2])
775-
/ "site-packages"
776-
)
764+
if self._site_packages is None:
765+
site_packages = []
766+
dist_packages = []
767+
for entry in self.sys_path:
768+
entry = Path(entry)
769+
if entry.name == "site-packages":
770+
site_packages.append(entry)
771+
elif entry.name == "dist-packages":
772+
dist_packages.append(entry)
773+
774+
if not site_packages and not dist_packages:
775+
raise RuntimeError("Unable to find the site-packages directory")
776+
777+
if site_packages:
778+
self._site_packages = site_packages[0]
779+
else:
780+
self._site_packages = dist_packages[0]
781+
782+
return self._site_packages
777783

778784
@property
779785
def sys_path(self): # type: () -> List[str]
@@ -1116,6 +1122,7 @@ def __init__(
11161122
os_name="posix",
11171123
is_venv=False,
11181124
pip_version="19.1",
1125+
sys_path=None,
11191126
**kwargs
11201127
):
11211128
super(MockEnv, self).__init__(**kwargs)
@@ -1126,6 +1133,7 @@ def __init__(
11261133
self._os_name = os_name
11271134
self._is_venv = is_venv
11281135
self._pip_version = Version.parse(pip_version)
1136+
self._sys_path = sys_path
11291137

11301138
@property
11311139
def version_info(self): # type: () -> Tuple[int]
@@ -1147,5 +1155,12 @@ def os(self): # type: () -> str
11471155
def pip_version(self):
11481156
return self._pip_version
11491157

1158+
@property
1159+
def sys_path(self):
1160+
if self._sys_path is None:
1161+
return super(MockEnv, self).sys_path
1162+
1163+
return self._sys_path
1164+
11501165
def is_venv(self): # type: () -> bool
11511166
return self._is_venv

tests/masonry/builders/test_editable.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
def test_build_should_delegate_to_pip_for_non_pure_python_packages(tmp_dir, mocker):
1818
move = mocker.patch("shutil.move")
1919
tmp_dir = Path(tmp_dir)
20-
env = MockEnv(path=tmp_dir, pip_version="18.1", execute=False)
21-
env.site_packages.mkdir(parents=True)
20+
env = MockEnv(path=tmp_dir, pip_version="18.1", execute=False, sys_path=[])
2221
module_path = fixtures_dir / "extended"
2322

2423
builder = EditableBuilder(Factory().create_poetry(module_path), env, NullIO())
@@ -33,8 +32,7 @@ def test_build_should_delegate_to_pip_for_non_pure_python_packages(tmp_dir, mock
3332
def test_build_should_temporarily_remove_the_pyproject_file(tmp_dir, mocker):
3433
move = mocker.patch("shutil.move")
3534
tmp_dir = Path(tmp_dir)
36-
env = MockEnv(path=tmp_dir, pip_version="19.1", execute=False)
37-
env.site_packages.mkdir(parents=True)
35+
env = MockEnv(path=tmp_dir, pip_version="19.1", execute=False, sys_path=[])
3836
module_path = fixtures_dir / "extended"
3937

4038
builder = EditableBuilder(Factory().create_poetry(module_path), env, NullIO())

tests/utils/test_env.py

+47-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@
3131
"""
3232

3333

34+
class MockVirtualEnv(VirtualEnv):
35+
def __init__(self, path, base=None, sys_path=None):
36+
super(MockVirtualEnv, self).__init__(path, base=base)
37+
38+
self._sys_path = sys_path
39+
40+
@property
41+
def sys_path(self):
42+
if self._sys_path is not None:
43+
return self._sys_path
44+
45+
return super(MockVirtualEnv, self).sys_path
46+
47+
3448
@pytest.fixture()
3549
def poetry(config):
3650
poetry = Factory().create_poetry(
@@ -786,7 +800,7 @@ def test_env_site_packages_should_find_the_site_packages_directory_if_standard(t
786800

787801
site_packages.mkdir(parents=True)
788802

789-
env = VirtualEnv(Path(tmp_dir), Path(tmp_dir))
803+
env = MockVirtualEnv(Path(tmp_dir), Path(tmp_dir), sys_path=[str(site_packages)])
790804

791805
assert site_packages == env.site_packages
792806

@@ -795,6 +809,37 @@ def test_env_site_packages_should_find_the_site_packages_directory_if_root(tmp_d
795809
site_packages = Path(tmp_dir).joinpath("site-packages")
796810
site_packages.mkdir(parents=True)
797811

798-
env = VirtualEnv(Path(tmp_dir), Path(tmp_dir))
812+
env = MockVirtualEnv(Path(tmp_dir), Path(tmp_dir), sys_path=[str(site_packages)])
813+
814+
assert site_packages == env.site_packages
815+
816+
817+
def test_env_site_packages_should_find_the_dist_packages_directory_if_necessary(
818+
tmp_dir,
819+
):
820+
site_packages = Path(tmp_dir).joinpath("dist-packages")
821+
site_packages.mkdir(parents=True)
822+
823+
env = MockVirtualEnv(Path(tmp_dir), Path(tmp_dir), sys_path=[str(site_packages)])
799824

800825
assert site_packages == env.site_packages
826+
827+
828+
def test_env_site_packages_should_prefer_site_packages_over_dist_packages(tmp_dir):
829+
dist_packages = Path(tmp_dir).joinpath("dist-packages")
830+
dist_packages.mkdir(parents=True)
831+
site_packages = Path(tmp_dir).joinpath("site-packages")
832+
site_packages.mkdir(parents=True)
833+
834+
env = MockVirtualEnv(
835+
Path(tmp_dir), Path(tmp_dir), sys_path=[str(dist_packages), str(site_packages)]
836+
)
837+
838+
assert site_packages == env.site_packages
839+
840+
841+
def test_env_site_packages_should_raise_an_error_if_no_site_packages(tmp_dir):
842+
env = MockVirtualEnv(Path(tmp_dir), Path(tmp_dir), sys_path=[])
843+
844+
with pytest.raises(RuntimeError):
845+
env.site_packages

0 commit comments

Comments
 (0)