Skip to content

Commit

Permalink
improvement: remove dependency on pkg_resources directly in the mod…
Browse files Browse the repository at this point in the history
…ule, although a transitive dependency (`nr.util.plugin`) still uses it
  • Loading branch information
NiklasRosenstein committed Dec 14, 2023
1 parent ebf6a8c commit 3470c88
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 50 deletions.
9 changes: 9 additions & 0 deletions .changelog/_unreleased.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,12 @@ pr = "https://github.com/NiklasRosenstein/slap/pull/103"
issues = [
"https://github.com/NiklasRosenstein/slap/issues/102",
]

[[entries]]
id = "d8af87d8-191f-40e8-8099-e5a5618cf230"
type = "improvement"
description = "remove dependency on `pkg_resources` directly in the module, although a transitive dependency (`nr.util.plugin`) still uses it"
author = "@NiklasRosenstein"
issues = [
"https://github.com/NiklasRosenstein/slap/issues/98",
]
15 changes: 8 additions & 7 deletions docs/content/commands/add.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ Poetry as the build backend, it will add `httpx = "^0.22.0"` wheras if it is usi

!!! note

Slap uses `pkg_resources.get_distribution()` to retrieve the version of the package that got installed, or was
already installed, and assumes that the package is available in the target Python environment.
Slap uses `importlib.metadata.distribution()` to retrieve the version of the package that got installed, or was
already installed, and assumes that the module is available in the target Python environment. The module is part
of the Python standard library starting with Python 3.8.

## Support matrix

| Build system | Supported |
| ------------ | --------- |
| Flit ||
| Poetry ||
| Setuptools | ❌ (dependencies defined in `setup.cfg`) |
| Build system | Supported |
|--------------|-----------------------------------------|
| Flit | |
| Poetry | |
| Setuptools | ❌ (dependencies defined in `setup.cfg`) |
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ ptyprocess = "^0.7.0"
pygments = "^2.11.2"
PyYAML = ">=6.0"
requests = "^2.27.1"
setuptools = ">=39.1.0" # Needed for pkg_resources
setuptools = ">=39.1.0" # Needed for setuptools backend
tomli = "^2.0.0"
tomlkit = "^0.12.1"
twine = "^3.7.0"
Expand Down
22 changes: 11 additions & 11 deletions src/slap/ext/application/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import json
import logging
import typing as t

import pkg_resources
from importlib.metadata import Distribution

from slap.application import Application, option
from slap.ext.application.venv import VenvAwareCommand
Expand Down Expand Up @@ -50,7 +49,7 @@ def handle(self) -> int:
for extra in extras:
requirements += project.dependencies().extra.get(extra, [])

dists_cache: dict[str, pkg_resources.Distribution | None] = {}
dists_cache: dict[str, Distribution | None] = {}
python_environment = PythonEnvironment.of("python")
requirements = filter_dependencies(requirements, python_environment.pep508, extras)
with tqdm.tqdm(desc="Resolving requirements graph") as progress:
Expand All @@ -67,15 +66,16 @@ def handle(self) -> int:
# Retrieve the license text from the distributions.
if self.option("with-license-text"):
for dist_name, dist_data in output["metadata"].items():
dist = dists_cache[dist_name]
if (dist := dists_cache[dist_name]) is None:
continue

dist_data["license_text"] = None
if dist is not None:
for filename in ("LICENSE", "LICENSE.txt", "LICENSE.text", "LICENSE.rst"):
try:
dist_data["license_text"] = dist.get_metadata(filename)
break
except FileNotFoundError:
pass
for file in dist.files or []:
if not file.parent.name.endswith(".dist-info"):
continue
if file.name == "LICENSE" or file.name.startswith("LICENSE."):
dist_data["license_text"] = file.read_text()
break

print(json.dumps(output, indent=2, sort_keys=True))
return 0
Expand Down
55 changes: 26 additions & 29 deletions src/slap/python/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
import subprocess as sp
import textwrap
import typing as t
from importlib.metadata import PathDistribution
from pathlib import Path

from slap.python import pep508

if t.TYPE_CHECKING:
import pkg_resources
from importlib.metadata import Distribution

from slap.python.dependency import Dependency

Expand All @@ -40,13 +41,13 @@ def is_venv(self) -> bool:

return bool(self.real_prefix or (self.base_prefix and self.prefix != self.base_prefix))

def has_pkg_resources(self) -> bool:
"""Checks if the Python environment has the `pkg_resources` module available."""
def has_importlib_metadata(self) -> bool:
"""Checks if the Python environment has the `importlib.metadata` module available."""

if self._has_pkg_resources is None:
code = textwrap.dedent(
"""
try: import pkg_resources
try: import importlib.metadata
except ImportError: print('false')
else: print('true')
"""
Expand Down Expand Up @@ -79,8 +80,8 @@ def of(python: str | t.Sequence[str]) -> "PythonEnvironment":
import sys, platform, json, pickle
sys.path.append({pep508_path!r})
import pep508
try: import pkg_resources
except ImportError: pkg_resources = None
try: import importlib.metadata as metadata
except ImportError: metadata = None
print(json.dumps({{
"executable": sys.executable,
"version": sys.version,
Expand All @@ -90,7 +91,7 @@ def of(python: str | t.Sequence[str]) -> "PythonEnvironment":
"base_prefix": getattr(sys, 'base_prefix', None),
"real_prefix": getattr(sys, 'real_prefix', None),
"pep508": pep508.Pep508Environment.current().as_json(),
"_has_pkg_resources": pkg_resources is not None,
"_has_pkg_resources": metadata is not None,
}}))
"""
)
Expand All @@ -100,25 +101,25 @@ def of(python: str | t.Sequence[str]) -> "PythonEnvironment":
payload["pep508"] = pep508.Pep508Environment(**payload["pep508"])
return PythonEnvironment(**payload)

def get_distribution(self, distribution: str) -> pkg_resources.Distribution | None:
def get_distribution(self, distribution: str) -> Distribution | None:
"""Query the details for a single distribution in the Python environment."""

return self.get_distributions([distribution])[distribution]

def get_distributions(self, distributions: t.Collection[str]) -> dict[str, pkg_resources.Distribution | None]:
def get_distributions(self, distributions: t.Collection[str]) -> dict[str, Distribution | None]:
"""Query the details for the given distributions in the Python environment with
#pkg_resources.get_distribution()."""
#importlib.metadata.distribution()."""

code = textwrap.dedent(
"""
import sys, pkg_resources, pickle
import sys, importlib.metadata as metadata, pickle
result = []
for arg in sys.argv[1:]:
try:
dist = pkg_resources.get_distribution(arg)
except pkg_resources.DistributionNotFound:
dist = metadata.distribution(arg)
except metadata.PackageNotFoundError:
dist = None
result.append(dist )
result.append(dist)
sys.stdout.buffer.write(pickle.dumps(result))
"""
)
Expand All @@ -132,7 +133,7 @@ def get_distributions(self, distributions: t.Collection[str]) -> dict[str, pkg_r
class DistributionMetadata:
"""Additional metadata for a distribution."""

location: str
location: str | None
version: str
license_name: str | None
platform: str | None
Expand All @@ -141,21 +142,17 @@ class DistributionMetadata:
extras: set[str]


def get_distribution_metadata(dist: pkg_resources.Distribution) -> DistributionMetadata:
def get_distribution_metadata(dist: Distribution) -> DistributionMetadata:
"""Parses the distribution metadata."""

from email.parser import Parser

data = Parser().parsestr(dist.get_metadata(dist.PKG_INFO))

return DistributionMetadata(
location=dist.location,
version=data.get("Version"),
license_name=data.get("License"),
platform=data.get("Platform"),
requires_python=data.get("Requires-Python"),
requirements=data.get_all("Requires-Dist") or [],
extras=set(data.get_all("Provides-Extra") or []),
location=str(dist._path) if isinstance(dist, PathDistribution) else None, # type: ignore[attr-defined]
version=dist.metadata["Version"],
license_name=dist.metadata.get("License"),
platform=dist.metadata.get("Platform"),
requires_python=dist.metadata.get("Requires-Python"),
requirements=dist.metadata.get_all("Requires-Dist") or [],
extras=set(dist.metadata.get_all("Provides-Extra") or []),
)


Expand Down Expand Up @@ -187,8 +184,8 @@ def update(self, other: DistributionGraph) -> None:
def build_distribution_graph(
env: PythonEnvironment,
dependencies: list[Dependency],
resolved_callback: t.Callable[[dict[str, pkg_resources.Distribution | None]], t.Any] | None = None,
dists_cache: dict[str, pkg_resources.Distribution | None] | None = None,
resolved_callback: t.Callable[[dict[str, Distribution | None]], t.Any] | None = None,
dists_cache: dict[str, Distribution | None] | None = None,
) -> DistributionGraph:
"""Builds a #DistributionGraph in the given #PythonEnvironment using the given dependencies.
Expand Down
2 changes: 1 addition & 1 deletion src/slap/templates/github/.github/workflows/python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.x"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v2
- uses: NiklasRosenstein/slap@gha/install/v1
Expand Down
2 changes: 1 addition & 1 deletion tests/slap/python/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ def test__PythonEnvironment__with_current_python_instance():
assert environment.prefix == sys.prefix
assert environment.base_prefix == getattr(sys, "base_prefix", None)
assert environment.real_prefix == getattr(sys, "real_prefix", None)
assert environment.has_pkg_resources() # Slap requires setuptools, so pkg_resources is definitely available
assert environment.has_importlib_metadata()
assert environment.get_distribution("setuptools") is not None

0 comments on commit 3470c88

Please sign in to comment.