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

feat: install plugins from project config #1893

Merged
merged 5 commits into from
May 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion docs/docs/dev/write.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Write a plugin
# PDM Plugins

PDM is aiming at being a community driven package manager.
It is shipped with a full-featured plug-in system, with which you can:
Expand Down Expand Up @@ -209,3 +209,31 @@ Hello, Jack
```

See more plugin management subcommands by typing `pdm self --help` in the terminal.

## Specify the plugins in project

To specify the required plugins for a project, you can use the `tool.pdm.plugins` config in the `pyproject.toml` file.
These dependencies can be installed into a project plugin library by running `pdm install --plugins`.
The project plugin library will be loaded in subsequent PDM commands.

This is useful when you want to share the same plugin set with the contributors.

```toml
# pyproject.toml
[tool.pdm]
plugins = [
"pdm-packer"
]
```

Run `pdm install --plugins` to install and activate the plugins.

Alternatively, you can have project-local plugins that are not published to PyPI, by using editable local dependencies:

```toml
# pyproject.toml
[tool.pdm]
plugins = [
"-e file:///${PROJECT_ROOT}/plugins/my_plugin"
]
```
1 change: 1 addition & 0 deletions news/1461.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Install project-level plugins from project config, with `tool.pdm.plugins` setting.
4 changes: 2 additions & 2 deletions src/pdm/builders/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from pyproject_hooks import BuildBackendHookCaller

from pdm.compat import tomllib
from pdm.environments import PrefixEnvironment
from pdm.environments import PythonEnvironment
from pdm.exceptions import BuildError
from pdm.models.in_process import get_sys_config_paths
from pdm.models.requirements import Requirement, parse_requirement
Expand Down Expand Up @@ -290,7 +290,7 @@ def install(self, requirements: Iterable[str], shared: bool = False) -> None:
if not missing:
return
path = self._prefix.shared if shared else self._prefix.overlay
env = PrefixEnvironment(self._env.project, prefix=path)
env = PythonEnvironment(self._env.project, prefix=path)
install_requirements(missing, env)

if shared:
Expand Down
25 changes: 25 additions & 0 deletions src/pdm/cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,36 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
action="store_true",
help="Check if the lock file is up to date and fail otherwise",
)
parser.add_argument("--plugins", action="store_true", help="Install the plugins specified in pyproject.toml")

def install_plugins(self, project: Project) -> None:
from pdm.environments import PythonEnvironment
from pdm.installers.core import install_requirements
from pdm.models.requirements import parse_requirement

plugins = [
parse_requirement(r[3:], True) if r.startswith("-e ") else parse_requirement(r)
for r in project.pyproject.plugins
]
if not plugins:
return
plugin_root = project.root / ".pdm-plugins"
environment = PythonEnvironment(project, python=sys.executable, prefix=str(plugin_root))
use_install_cache = project.config["install.cache"]
with project.core.ui.open_spinner("[success]Installing plugins...[/]"):
with project.core.ui.logging("install-plugins"):
install_requirements(plugins, environment, use_install_cache=use_install_cache, clean=True)
if not plugin_root.joinpath(".gitignore").exists():
plugin_root.joinpath(".gitignore").write_text("*\n")
project.core.ui.echo("Plugins are installed successfully into [primary].pdm-plugins[/].")

def handle(self, project: Project, options: argparse.Namespace) -> None:
if not project.pyproject.is_valid and termui.is_interactive():
actions.ask_for_import(project)

if options.plugins:
return self.install_plugins(project)

hooks = HookManager(project, options.skip)

strategy = actions.check_lockfile(project, False)
Expand Down
2 changes: 1 addition & 1 deletion src/pdm/cli/completions/pdm.bash
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ _pdm_a919b69078acdf0a_complete()
;;

(install)
opts="--check --dev --dry-run --fail-fast --global --group --help --lockfile --no-default --no-editable --no-isolation --no-lock --no-self --production --project --skip --venv --verbose"
opts="--check --dev --dry-run --fail-fast --global --group --help --lockfile --no-default --no-editable --no-isolation --no-lock --no-self --plugins --production --project --skip --venv --verbose"
;;

(list)
Expand Down
1 change: 1 addition & 0 deletions src/pdm/cli/completions/pdm.fish
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-editable -d 'I
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-isolation -d 'Do not isolate the build in a clean environment'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-lock -d 'Don\'t do lock if the lock file is not found or outdated'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-self -d 'Don\'t install the project itself'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l plugins -d 'Install the plugins specified in pyproject.toml'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l production -d 'Unselect dev dependencies'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use ":all" to skip all hooks. Use ":pre" and ":post" to skip all pre or post hooks.'
Expand Down
2 changes: 1 addition & 1 deletion src/pdm/cli/completions/pdm.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ function TabExpansion($line, $lastWord) {
[Option]::new((
"-d", "--dev", "-g", "--global", "--dry-run", "--no-default", "--no-lock", "--prod",
"--production", "--no-editable", "--no-self", "--no-isolation", "--check", "-L",
"--lockfile", "--fail-fast", "-x"
"--lockfile", "--fail-fast", "-x", "--plugins"
)),
$sectionOption,
$skipOption,
Expand Down
1 change: 1 addition & 0 deletions src/pdm/cli/completions/pdm.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ _pdm() {
"--no-isolation[do not isolate the build in a clean environment]"
"--dry-run[Show the difference only without modifying the lock file content]"
"--check[Check if the lock file is up to date and fail otherwise]"
"--plugins[Install the plugins specified in pyproject.toml]"
'--venv[Run the command in the virtual environment with the given key. (env var: PDM_IN_VENV)]:venv:'
)
;;
Expand Down
17 changes: 5 additions & 12 deletions src/pdm/cli/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import argparse
import dataclasses as dc
import json
import os
import sys
Expand Down Expand Up @@ -149,25 +150,17 @@ def _parse_known_args(
raise PdmArgumentError(e) from e


@dc.dataclass(frozen=True)
class Package:
"""An internal class for the convenience of dependency graph building."""

def __init__(self, name: str, version: str | None, requirements: dict[str, Requirement]) -> None:
self.name = name
self.version = version # if version is None, the dist is not installed.
self.requirements = requirements

def __hash__(self) -> int:
return hash(self.name)
name: str = dc.field(hash=True, compare=True)
version: str | None = dc.field(compare=False)
requirements: dict[str, Requirement] = dc.field(compare=False)

def __repr__(self) -> str:
return f"<Package {self.name}=={self.version}>"

def __eq__(self, value: object) -> bool:
if not isinstance(value, Package):
return False
return self.name == value.name


def build_dependency_graph(
working_set: Mapping[str, im.Distribution],
Expand Down
14 changes: 14 additions & 0 deletions src/pdm/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,19 @@ def add_config(name: str, config_item: ConfigItem) -> None:
"""
Config.add_config(name, config_item)

def _add_project_plugins_library(self) -> None:
project = self.create_project(is_global=False)
if project.is_global or not project.root.joinpath(".pdm-plugins").exists():
return

import site
import sysconfig

base = str(project.root / ".pdm-plugins")
replace_vars = {"base": base, "platbase": base}
scheme = "nt" if os.name == "nt" else "posix_prefix"
site.addsitedir(sysconfig.get_path("purelib", scheme, replace_vars))

def load_plugins(self) -> None:
"""Import and load plugins under `pdm.plugin` namespace
A plugin is a callable that accepts the core object as the only argument.
Expand All @@ -234,6 +247,7 @@ def my_plugin(core: pdm.core.Core) -> None:
...
```
"""
self._add_project_plugins_library()
entry_points: Iterable[importlib_metadata.EntryPoint] = itertools.chain(
importlib_metadata.entry_points(group="pdm"),
importlib_metadata.entry_points(group="pdm.plugin"),
Expand Down
2 changes: 0 additions & 2 deletions src/pdm/environments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from pdm.environments.base import BareEnvironment, BaseEnvironment
from pdm.environments.local import PythonLocalEnvironment
from pdm.environments.prefix import PrefixEnvironment
from pdm.environments.python import PythonEnvironment

_deprecated = {"Environment": PythonLocalEnvironment, "GlobalEnvironment": PythonEnvironment}
Expand All @@ -26,7 +25,6 @@ def __getattr__(name: str) -> Any:
"BaseEnvironment",
"BareEnvironment",
"PythonEnvironment",
"PrefixEnvironment",
"PythonLocalEnvironment",
]
__all__.extend(_deprecated)
19 changes: 7 additions & 12 deletions src/pdm/environments/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class BaseEnvironment(abc.ABC):

is_local = False

def __init__(self, project: Project) -> None:
def __init__(self, project: Project, *, python: str | None = None) -> None:
"""
:param project: the project instance
"""
Expand All @@ -43,6 +43,10 @@ def __init__(self, project: Project) -> None:
self.project.sources,
self.project.core.ui.verbosity >= termui.Verbosity.DETAIL,
)
if python is None:
self._interpreter = project.python
else:
self._interpreter = PythonInfo.from_path(python)

@property
def is_global(self) -> bool:
Expand All @@ -51,7 +55,7 @@ def is_global(self) -> bool:

@property
def interpreter(self) -> PythonInfo:
return self.project.python
return self._interpreter

@abc.abstractmethod
def get_paths(self) -> dict[str, str]:
Expand Down Expand Up @@ -207,16 +211,7 @@ class BareEnvironment(BaseEnvironment):
"""Bare environment that does not depend on project files."""

def __init__(self, project: Project) -> None:
self.python_requires = project.python_requires
self.project = project
self.auth = PdmBasicAuth(
self.project.sources,
self.project.core.ui.verbosity >= termui.Verbosity.DETAIL,
)

@property
def interpreter(self) -> PythonInfo:
return PythonInfo.from_path(sys.executable)
super().__init__(project, python=sys.executable)

def get_paths(self) -> dict[str, str]:
return {}
Expand Down
27 changes: 0 additions & 27 deletions src/pdm/environments/prefix.py

This file was deleted.

29 changes: 12 additions & 17 deletions src/pdm/environments/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from pdm.environments.base import BaseEnvironment
from pdm.models.in_process import get_sys_config_paths
from pdm.models.python import PythonInfo

if TYPE_CHECKING:
from pdm.project import Project
Expand All @@ -14,24 +13,20 @@
class PythonEnvironment(BaseEnvironment):
"""A project environment that is directly derived from a Python interpreter"""

def __init__(self, project: Project, *, python: str | None = None) -> None:
super().__init__(project)
if python is None:
self._interpreter = project.python
else:
self._interpreter = PythonInfo.from_path(python)

@property
def interpreter(self) -> PythonInfo:
return self._interpreter
def __init__(self, project: Project, *, python: str | None = None, prefix: str | None = None) -> None:
super().__init__(project, python=python)
self.prefix = prefix

def get_paths(self) -> dict[str, str]:
is_venv = self.interpreter.get_venv() is not None
paths = get_sys_config_paths(
str(self.interpreter.executable),
kind="user" if not is_venv and self.project.global_config["global_project.user_site"] else "default",
)
if is_venv:
if self.prefix is not None:
replace_vars = {"base": self.prefix, "platbase": self.prefix}
kind = "prefix"
else:
replace_vars = None
kind = "user" if not is_venv and self.project.global_config["global_project.user_site"] else "default"
paths = get_sys_config_paths(str(self.interpreter.executable), replace_vars, kind=kind)
if is_venv and self.prefix is None:
python_xy = f"python{self.interpreter.identifier}"
paths["include"] = os.path.join(paths["data"], "include", "site", python_xy)
paths["prefix"] = paths["data"]
Expand All @@ -42,6 +37,6 @@ def get_paths(self) -> dict[str, str]:
def process_env(self) -> dict[str, str]:
env = super().process_env
venv = self.interpreter.get_venv()
if venv is not None:
if venv is not None and self.prefix is None:
env.update(venv.env_vars())
return env
21 changes: 4 additions & 17 deletions src/pdm/installers/core.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
from __future__ import annotations

from pdm.environments import BaseEnvironment
from pdm.installers.manager import InstallManager
from pdm.installers.synchronizers import BaseSynchronizer
from pdm.models.requirements import Requirement
from pdm.models.specifiers import PySpecSet
from pdm.resolver.core import resolve
from pdm.termui import logger


def install_requirements(
reqs: list[Requirement],
environment: BaseEnvironment,
use_install_cache: bool = False,
reqs: list[Requirement], environment: BaseEnvironment, use_install_cache: bool = False, clean: bool = False
) -> None: # pragma: no cover
"""Resolve and install the given requirements into the environment."""
project = environment.project
Expand All @@ -31,15 +28,5 @@ def install_requirements(
environment.python_requires,
max_rounds=resolve_max_rounds,
)
manager = InstallManager(environment, use_install_cache=use_install_cache)
working_set = environment.get_working_set()
for key, candidate in resolved.items():
if "[" in key:
# This is a candidate with extras, just skip it as it will be handled
# by the one without extras.
continue
logger.info("Installing %s %s", candidate.name, candidate.version)
if key in working_set:
# Force reinstall the package if it's already installed.
manager.uninstall(working_set[key])
manager.install(candidate)
syncer = BaseSynchronizer(resolved, environment, clean=clean, use_install_cache=use_install_cache, retry_times=0)
syncer.synchronize()
Loading