Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use importlib.metadata #344

Closed
wants to merge 9 commits into from
4 changes: 3 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
0.15.1.4
0.15.2.0

- Rely on importlib.metadata for package metadata.
- [bugfix] Replaced implicit dependency on setuptools with an explicit dependency on packaging (#339).

0.15.1.3
Expand Down
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@

CURDIR = Path(__file__).parent

REQUIRED = ["userpath", "argcomplete>=1.9.4, <2.0", "packaging"] # type: List[str]
REQUIRED = [
"userpath",
"argcomplete>=1.9.4, <2.0",
"packaging",
"importlib_metadata; python_version < '3.8'",
] # type: List[str]

with io.open(os.path.join(CURDIR, "README.md"), "r", encoding="utf-8") as f:
README = f.read()
Expand Down
2 changes: 2 additions & 0 deletions src/pipx/shared_libs.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ def upgrade(self, pip_args: List[str], verbose: bool = False):
"pip",
"setuptools",
"wheel",
"packaging",
"importlib_metadata",
]
)
self.has_been_updated_this_run = True
Expand Down
103 changes: 53 additions & 50 deletions src/pipx/venv_metadata_inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
from pathlib import Path
from typing import Dict, List, Optional

try:
from importlib import metadata # type: ignore
except ImportError:
import importlib_metadata as metadata # type: ignore

from packaging.requirements import Requirement # type: ignore

try:
WindowsError
Expand All @@ -14,62 +20,59 @@
WINDOWS = True


def get_package_dependencies(package: str) -> List[str]:
def get_package_dependencies(package: str) -> List[Requirement]:
extras = Requirement(package).extras
try:
import pkg_resources
return [
req
for req in map(Requirement, metadata.requires(package) or []) # type: ignore
if not req.marker or req.marker.evaluate({"extra": extras})
]
except Exception:
return []
return [str(r) for r in pkg_resources.get_distribution(package).requires()]


def get_package_version(package: str) -> Optional[str]:
try:
import pkg_resources

return pkg_resources.get_distribution(package).version
return metadata.version(package) # type: ignore
except Exception:
return None


def get_apps(package: str, bin_path: Path) -> List[str]:
try:
import pkg_resources
except Exception:
return []
dist = pkg_resources.get_distribution(package)
def get_apps(req: Requirement, bin_path: Path) -> List[str]:
dist = metadata.distribution(req.name) # type: ignore

apps = set()
for section in ["console_scripts", "gui_scripts"]:
# "entry_points" entry in setup.py are found here
for name in pkg_resources.get_entry_map(dist).get(section, []):
if (bin_path / name).exists():
apps.add(name)
if WINDOWS and (bin_path / (name + ".exe")).exists():
# WINDOWS adds .exe to entry_point name
apps.add(name + ".exe")

if dist.has_metadata("RECORD"):
# for non-editable package installs, RECORD is list of installed files
# "scripts" entry in setup.py is found here (test w/ awscli)
for line in dist.get_metadata_lines("RECORD"):
entry = line.split(",")[0] # noqa: T484
path = (Path(dist.location) / entry).resolve()
try:
if path.parent.samefile(bin_path):
apps.add(Path(entry).name)
except FileNotFoundError:
pass

if dist.has_metadata("installed-files.txt"):
# not sure what is found here
for line in dist.get_metadata_lines("installed-files.txt"):
entry = line.split(",")[0] # noqa: T484
path = (Path(dist.egg_info) / entry).resolve() # type: ignore
try:
if path.parent.samefile(bin_path):
apps.add(Path(entry).name)
except FileNotFoundError:
pass
sections = {"console_scripts", "gui_scripts"}
# "entry_points" entry in setup.py are found here
for ep in dist.entry_points:
if ep.group not in sections:
continue
if (bin_path / ep.name).exists():
apps.add(ep.name)
if WINDOWS and (bin_path / (ep.name + ".exe")).exists():
# WINDOWS adds .exe to entry_point name
apps.add(ep.name + ".exe")

# search installed files
# "scripts" entry in setup.py is found here (test w/ awscli)
for path in dist.files:
try:
if path.locate().parent.samefile(bin_path):
apps.add(path.name)
except FileNotFoundError:
pass

# not sure what is found here
inst_files = dist.read_text("installed-files.txt") or ""
for line in inst_files.splitlines():
entry = line.split(",")[0] # noqa: T484
path = dist.locate_file(entry) # type: ignore
try:
if path.parent.samefile(bin_path):
apps.add(Path(entry).name)
except FileNotFoundError:
pass

return sorted(apps)

Expand All @@ -84,17 +87,17 @@ def _dfs_package_apps(
dep_visited = {}

dependencies = get_package_dependencies(package)
for d in dependencies:
app_names = get_apps(d, bin_path)
for req in dependencies:
app_names = get_apps(req, bin_path)
if app_names:
app_paths_of_dependencies[d] = [bin_path / app for app in app_names]
app_paths_of_dependencies[req.name] = [bin_path / app for app in app_names]
# recursively search for more
if d not in dep_visited:
if req.name not in dep_visited:
# only search if this package isn't already listed to avoid
# infinite recursion
dep_visited[d] = True
dep_visited[req.name] = True
app_paths_of_dependencies = _dfs_package_apps(
bin_path, d, app_paths_of_dependencies, dep_visited
bin_path, req.name, app_paths_of_dependencies, dep_visited
)
return app_paths_of_dependencies

Expand All @@ -119,7 +122,7 @@ def main():
package = sys.argv[1]
bin_path = Path(sys.argv[2])

apps = get_apps(package, bin_path)
apps = get_apps(Requirement(package), bin_path)
app_paths = [Path(bin_path) / app for app in apps]
if WINDOWS:
app_paths = _windows_extra_app_paths(app_paths)
Expand Down
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest # type: ignore

from pipx import constants
from pipx import constants, shared_libs


@pytest.fixture
Expand All @@ -17,6 +17,8 @@ def pipx_temp_env(tmp_path, monkeypatch):
bin_dir = Path(tmp_path) / "otherdir" / "pipxbindir"

monkeypatch.setattr(constants, "PIPX_SHARED_LIBS", shared_dir)
shared_libs.PIPX_SHARED_LIBS = shared_dir
shared_libs.shared_libs.__init__()
monkeypatch.setattr(constants, "PIPX_HOME", home_dir)
monkeypatch.setattr(constants, "LOCAL_BIN_DIR", bin_dir)
monkeypatch.setattr(constants, "PIPX_LOCAL_VENVS", home_dir / "venvs")
Expand Down