diff --git a/README.md b/README.md index c65ab67..2247ce3 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ with the `@plugin` decorator, you can expose functions as plugins. They will be into `FunctionPlugin` instances, which satisfy both the contract of a Plugin, and that of the function. ```python -from plugin import plugin +from plux import plugin @plugin(namespace="localstack.configurators") def configure_logging(runtime): diff --git a/plux/build/discovery.py b/plux/build/discovery.py index 63b9b7e..2a8b44f 100644 --- a/plux/build/discovery.py +++ b/plux/build/discovery.py @@ -1,24 +1,110 @@ """ -Buildtool independent utils to discover plugins from the codebase, and write index files. +Buildtool independent utils to discover plugins from the project's source code. """ -import configparser +import importlib import inspect -import json import logging -import sys import typing as t +from fnmatch import fnmatchcase from types import ModuleType +import os +import pkgutil from plux import PluginFinder, PluginSpecResolver, PluginSpec -from plux.core.entrypoint import discover_entry_points, EntryPointDict - -if t.TYPE_CHECKING: - from _typeshed import SupportsWrite LOG = logging.getLogger(__name__) +class PackageFinder: + """ + Generate a list of Python packages. How these are generated depends on the implementation. + + Why this abstraction? The naive way to find packages is to list all directories with an ``__init__.py`` file. + However, this approach does not work for distributions that have namespace packages. How do we know whether + something is a namespace package or just a directory? Typically, the build configuration will tell us. For example, + setuptools has the following directives for ``pyproject.toml``:: + + [tool.setuptools] + packages = ["mypkg", "mypkg.subpkg1", "mypkg.subpkg2"] + + Or in hatch:: + + [tool.hatch.build.targets.wheel] + packages = ["src/foo"] + + So this abstraction allows us to use the build tool internals to generate a list of packages that we should be + scanning for plugins. + """ + + def find_packages(self) -> t.Iterable[str]: + """ + Returns an Iterable of Python packages. Each item is a string-representation of a Python package (for example, + ``plux.core``, ``myproject.mypackage.utils``, ...) + + :return: An Iterable of Packages + """ + raise NotImplementedError + + @property + def path(self) -> str: + """ + The root file path under which the packages are located. + + :return: A file path + """ + raise NotImplementedError + + +class PluginFromPackageFinder(PluginFinder): + """ + Finds Plugins from packages that are resolved by the given ``PackageFinder``. Under the hood this uses a + ``ModuleScanningPluginFinder``, which, for each package returned by the ``PackageFinder``, imports the package using + ``importlib``, and scans the module for plugins. + """ + + finder: PackageFinder + + def __init__(self, finder: PackageFinder): + self.finder = finder + + def find_plugins(self) -> list[PluginSpec]: + collector = ModuleScanningPluginFinder(self._load_modules()) + return collector.find_plugins() + + def _load_modules(self) -> t.Generator[ModuleType, None, None]: + """ + Generator to load all imported modules that are part of the packages returned by the ``PackageFinder``. + + :return: A generator of python modules + """ + for module_name in self._list_module_names(): + try: + yield importlib.import_module(module_name) + except Exception as e: + LOG.error("error importing module %s: %s", module_name, e) + + def _list_module_names(self) -> set[str]: + """ + This method creates a set of module names by iterating over the packages detected by the ``PackageFinder``. It + includes top-level packages, as well as submodules found within those packages. + + :return: A set of strings where each string represents a module name. + """ + # adapted from https://stackoverflow.com/a/54323162/804840 + + modules = set() + + for pkg in self.finder.find_packages(): + modules.add(pkg) + pkgpath = self.finder.path.rstrip(os.sep) + os.sep + pkg.replace(".", os.sep) + for info in pkgutil.iter_modules([pkgpath]): + if not info.ispkg: + modules.add(pkg + "." + info.name) + + return modules + + class ModuleScanningPluginFinder(PluginFinder): """ A PluginFinder that scans the members of given modules for available PluginSpecs. Each member is evaluated with a @@ -51,72 +137,27 @@ def find_plugins(self) -> list[PluginSpec]: return plugins -class PluginIndexBuilder: +class Filter: """ - Builds an index file containing all discovered plugins. The index file can be written to stdout, or to a file. - The writer supports two formats: json and ini. + Given a list of patterns, create a callable that will be true only if + the input matches at least one of the patterns. + This is from `setuptools.discovery._Filter` """ - def __init__( - self, - plugin_finder: PluginFinder, - output_format: t.Literal["json", "ini"] = "json", - ): - self.plugin_finder = plugin_finder - self.output_format = output_format + def __init__(self, patterns: t.Iterable[str]): + self._patterns = patterns - def write(self, fp: "SupportsWrite[str]" = sys.stdout) -> EntryPointDict: - """ - Discover entry points using the configured ``PluginFinder``, and write the entry points into a file. + def __call__(self, item: str): + return any(fnmatchcase(item, pat) for pat in self._patterns) - :param fp: The file-like object to write to. - :return: The discovered entry points that were written into the file. - """ - ep = discover_entry_points(self.plugin_finder) - # sort entrypoints alphabetically in each group first - for group in ep: - ep[group].sort() - - if self.output_format == "json": - json.dump(ep, fp, sort_keys=True, indent=2) - elif self.output_format == "ini": - cfg = configparser.ConfigParser() - cfg.read_dict(self.convert_to_nested_entry_point_dict(ep)) - cfg.write(fp) - else: - raise ValueError(f"unknown plugin index output format {self.output_format}") +class MatchAllFilter(Filter): + """ + Filter that is equivalent to ``_Filter(["*"])``. + """ - return ep + def __init__(self): + super().__init__([]) - @staticmethod - def convert_to_nested_entry_point_dict(ep: EntryPointDict) -> dict[str, dict[str, str]]: - """ - Converts and ``EntryPointDict`` to a nested dict, where the keys are the section names and values are - dictionaries. Each dictionary maps entry point names to their values. It also sorts the output alphabetically. - - Example: - Input EntryPointDict: - { - 'console_scripts': ['app=module:main', 'tool=module:cli'], - 'plux.plugins': ['plugin1=pkg.module:Plugin1'] - } - - Output nested dict: - { - 'console_scripts': { - 'app': 'module:main', - 'tool': 'module:cli' - }, - 'plux.plugins': { - 'plugin1': 'pkg.module:Plugin1' - } - } - """ - result = {} - for section_name in sorted(ep.keys()): - result[section_name] = {} - for entry_point in sorted(ep[section_name]): - name, value = entry_point.split("=") - result[section_name][name] = value - return result + def __call__(self, item: str): + return True diff --git a/plux/build/hatchling.py b/plux/build/hatchling.py new file mode 100644 index 0000000..10509b5 --- /dev/null +++ b/plux/build/hatchling.py @@ -0,0 +1,6 @@ +from plux.build.project import Project + + +class HatchlingProject(Project): + # TODO: implement me + pass diff --git a/plux/build/index.py b/plux/build/index.py new file mode 100644 index 0000000..79e9990 --- /dev/null +++ b/plux/build/index.py @@ -0,0 +1,89 @@ +""" +Code to manage the plux plugin index file. The index file contains all discovered plugins, which later is used to +generate entry points. +""" + +import configparser +import json +import sys +import typing as t + +from plux.core.plugin import PluginFinder +from plux.core.entrypoint import EntryPointDict, discover_entry_points + +if t.TYPE_CHECKING: + from _typeshed import SupportsWrite + + +class PluginIndexBuilder: + """ + Builds an index file containing all discovered plugins. The index file can be written to stdout, or to a file. + The writer supports two formats: json and ini. + """ + + def __init__( + self, + plugin_finder: PluginFinder, + ): + self.plugin_finder = plugin_finder + + def write( + self, + fp: "SupportsWrite[str]" = sys.stdout, + output_format: t.Literal["json", "ini"] = "json", + ) -> EntryPointDict: + """ + Discover entry points using the configured ``PluginFinder``, and write the entry points into a file. + + :param fp: The file-like object to write to. + :param output_format: The format to write the entry points in. Can be either "json" or "ini". + :return: The discovered entry points that were written into the file. + """ + ep = discover_entry_points(self.plugin_finder) + + # sort entrypoints alphabetically in each group first + for group in ep: + ep[group].sort() + + if output_format == "json": + json.dump(ep, fp, sort_keys=True, indent=2) + elif output_format == "ini": + cfg = configparser.ConfigParser() + cfg.read_dict(self.convert_to_nested_entry_point_dict(ep)) + cfg.write(fp) + else: + raise ValueError(f"unknown plugin index output format {output_format}") + + return ep + + @staticmethod + def convert_to_nested_entry_point_dict(ep: EntryPointDict) -> dict[str, dict[str, str]]: + """ + Converts and ``EntryPointDict`` to a nested dict, where the keys are the section names and values are + dictionaries. Each dictionary maps entry point names to their values. It also sorts the output alphabetically. + + Example: + Input EntryPointDict: + { + 'console_scripts': ['app=module:main', 'tool=module:cli'], + 'plux.plugins': ['plugin1=pkg.module:Plugin1'] + } + + Output nested dict: + { + 'console_scripts': { + 'app': 'module:main', + 'tool': 'module:cli' + }, + 'plux.plugins': { + 'plugin1': 'pkg.module:Plugin1' + } + } + """ + result = {} + for section_name in sorted(ep.keys()): + result[section_name] = {} + for entry_point in sorted(ep[section_name]): + name, value = entry_point.split("=") + result[section_name][name] = value + return result diff --git a/plux/build/project.py b/plux/build/project.py new file mode 100644 index 0000000..f52bff7 --- /dev/null +++ b/plux/build/project.py @@ -0,0 +1,76 @@ +import os +from pathlib import Path + +from plux.build.config import PluxConfiguration, read_plux_config_from_workdir +from plux.build.discovery import PackageFinder, PluginFromPackageFinder +from plux.build.index import PluginIndexBuilder + + +class Project: + """ + Abstraction for a python project to hide the details of a build tool from the CLI. A Project provides access to + the project's configuration, package discovery, and the entrypoint build mechanism. + """ + + workdir: Path + config: PluxConfiguration + + def __init__(self, workdir: str = None): + self.workdir = Path(workdir or os.curdir) + self.config = self.read_static_plux_config() + + def find_entry_point_file(self) -> Path: + """ + Finds the entry_point.txt file of the current project. In case of setuptools, this may be in the + ``.egg-info`` directory, in case of hatch, where ``pip install -e .`` has become the standard, the + entrypoints file lives in the ``.dist-info`` directory of the venv. + + :return: A path pointing to the entrypoints file. The file might not exist. + """ + raise NotImplementedError + + def find_plux_index_file(self) -> Path: + """ + Returns the plux index file location. This may depend on the build tool, for similar reasons described in + ``find_entry_point_file``. For example, in setuptools, the plux index file by default is in + ``.egg-info/plux.json``. + + :return: A path pointing to the plux index file. + """ + raise NotImplementedError + + def create_package_finder(self) -> PackageFinder: + """ + Returns a build tool-specific PackageFinder instance that can be used to discover packages to scan for plugins. + + :return: A PackageFinder instance + """ + raise NotImplementedError + + def build_entrypoints(self): + """ + Routine to build the entrypoints file using ``EntryPointBuildMode.BUILD_HOOK``. This is called by the CLI + frontend. It's build tool-specific since we need to hook into the build process that generates the + ``entry_points.txt``. + """ + raise NotImplementedError + + def create_plugin_index_builder(self) -> PluginIndexBuilder: + """ + Returns a PluginIndexBuilder instance that can be used to build the plugin index. + + The default implementation creates a PluginFromPackageFinder instance using ``create_package_finder``. + + :return: A PluginIndexBuilder instance. + """ + plugin_finder = PluginFromPackageFinder(self.create_package_finder()) + return PluginIndexBuilder(plugin_finder) + + def read_static_plux_config(self) -> PluxConfiguration: + """ + Reads the static configuration (``pyproject.toml``) from the Project's working directory using + ``read_read_plux_config_from_workdir``. + + :return: A PluxConfiguration object + """ + return read_plux_config_from_workdir(str(self.workdir)) diff --git a/plux/build/setuptools.py b/plux/build/setuptools.py index 1598d29..274adf7 100644 --- a/plux/build/setuptools.py +++ b/plux/build/setuptools.py @@ -2,7 +2,6 @@ Bindings to integrate plux into setuptools build processes. """ -import importlib import json import logging import os @@ -10,14 +9,14 @@ import shutil import sys import typing as t -from fnmatch import fnmatchcase from pathlib import Path import setuptools from setuptools.command.egg_info import egg_info from plux.build import config -from .config import EntrypointBuildMode +from plux.build.config import EntrypointBuildMode +from plux.build.project import Project try: from setuptools.command.editable_wheel import editable_wheel @@ -35,9 +34,9 @@ def _ensure_dist_info(self, *args, **kwargs): from setuptools.command.egg_info import InfoCommon, write_entries from plux.core.entrypoint import EntryPointDict, discover_entry_points -from plux.core.plugin import PluginFinder, PluginSpec from plux.runtime.metadata import entry_points_from_metadata_path -from .discovery import ModuleScanningPluginFinder, PluginIndexBuilder +from plux.build.discovery import PluginFromPackageFinder, PackageFinder, Filter, MatchAllFilter +from plux.build.index import PluginIndexBuilder LOG = logging.getLogger(__name__) @@ -102,8 +101,8 @@ def finalize_options(self) -> None: ) def run(self) -> None: - index_builder = create_plugin_index_builder(self.plux_config, self.distribution, output_format="json") - + # TODO: should be reconciled with Project.build_entrypoints() + index_builder = create_plugin_index_builder(self.plux_config, self.distribution) self.debug_print(f"writing discovered plugins into {self.plux_json_path}") self.mkpath(os.path.dirname(self.plux_json_path)) with open(self.plux_json_path, "w") as fp: @@ -304,6 +303,52 @@ def load_entry_points(where=".", exclude=(), include=("*",), merge: EntryPointDi # The remaining methods are utilities +class SetuptoolsProject(Project): + distribution: setuptools.Distribution + + def __init__(self, workdir: str = None): + super().__init__(workdir) + + self.distribution = get_distribution_from_workdir(str(self.workdir)) + + def find_entry_point_file(self) -> Path: + if egg_info_dir := find_egg_info_dir(): + return Path(egg_info_dir, "entry_points.txt") + raise FileNotFoundError("No .egg-info directory found. Have you run `python -m plux entrypoints`?") + + def find_plux_index_file(self) -> Path: + if self.config.entrypoint_build_mode == EntrypointBuildMode.MANUAL: + return self.workdir / self.config.entrypoint_static_file + + return Path(get_plux_json_path(self.distribution)) + + def create_plugin_index_builder(self) -> PluginIndexBuilder: + return create_plugin_index_builder(self.config, self.distribution) + + def create_package_finder(self) -> PackageFinder: + exclude = [_path_to_module(item) for item in self.config.exclude] + include = [_path_to_module(item) for item in self.config.include] + return DistributionPackageFinder(self.distribution, exclude=exclude, include=include) + + def build_entrypoints(self): + dist = self.distribution + + dist.command_options["plugins"] = { + "exclude": ("command line", ",".join(self.config.exclude) or None), + "include": ("command line", ",".join(self.config.include) or None), + } + dist.run_command("plugins") + + print(f"building {dist.get_name().replace('-', '_')}.egg-info...") + dist.run_command("egg_info") + + print("discovered plugins:") + # print discovered plux plugins + with open(get_plux_json_path(dist)) as fd: + plux_json = json.load(fd) + json.dump(plux_json, sys.stdout, indent=2) + + def get_plux_json_path(distribution: setuptools.Distribution) -> str: """ Returns the full path of ``plux.json`` file for the given distribution. The file is located within the .egg-info @@ -345,7 +390,6 @@ def update_entrypoints(distribution: setuptools.Distribution, ep: EntryPointDict def create_plugin_index_builder( cfg: config.PluxConfiguration, distribution: setuptools.Distribution, - output_format: t.Literal["json", "ini"] = "json", ) -> PluginIndexBuilder: """ Creates a PluginIndexBuilder instance for discovering plugins from a setuptools distribution. It uses a @@ -357,8 +401,7 @@ def create_plugin_index_builder( plugin_finder = PluginFromPackageFinder( DistributionPackageFinder(distribution, exclude=exclude, include=include) ) - - return PluginIndexBuilder(plugin_finder, output_format) + return PluginIndexBuilder(plugin_finder) def entry_points_from_egg_info(egg_info_dir: str) -> EntryPointDict: @@ -491,46 +534,7 @@ def _path_to_module(path): return ".".join(Path(path).with_suffix("").parts) -class _Filter: - """ - Given a list of patterns, create a callable that will be true only if - the input matches at least one of the patterns. - This is from `setuptools.discovery._Filter` - """ - - def __init__(self, patterns: t.Iterable[str]): - self._patterns = patterns - - def __call__(self, item: str): - return any(fnmatchcase(item, pat) for pat in self._patterns) - - -class _MatchAllFilter(_Filter): - """ - Filter that is equivalent to ``_Filter(["*"])``. - """ - - def __init__(self): - super().__init__([]) - - def __call__(self, item: str): - return True - - -class _PackageFinder: - """ - Generate a list of Python packages. How these are generated depends on the implementation. - """ - - def find_packages(self) -> t.Iterable[str]: - raise NotImplementedError - - @property - def path(self) -> str: - raise NotImplementedError - - -class DistributionPackageFinder(_PackageFinder): +class DistributionPackageFinder(PackageFinder): """ PackageFinder that returns the packages found in the distribution. The Distribution will already have a list of resolved packages depending on the setup config. For example, if a ``pyproject.toml`` is used, @@ -550,10 +554,14 @@ def __init__( include: t.Iterable[str] | None = None, ): self.distribution = distribution - self.exclude = _Filter(exclude or []) - self.include = _Filter(include) if include else _MatchAllFilter() + self.exclude = Filter(exclude or []) + self.include = Filter(include) if include else MatchAllFilter() def find_packages(self) -> t.Iterable[str]: + if self.distribution.packages is None: + raise ValueError( + "No packages found in setuptools distribution. Is your project configured correctly?" + ) return self.filter_packages(self.distribution.packages) @property @@ -572,7 +580,11 @@ def filter_packages(self, packages: t.Iterable[str]) -> t.Iterable[str]: return [item for item in packages if not self.exclude(item) and self.include(item)] -class DefaultPackageFinder(_PackageFinder): +class SetuptoolsPackageFinder(PackageFinder): + """ + Uses setuptools internals to resolve packages. + """ + def __init__(self, where=".", exclude=(), include=("*",), namespace=True) -> None: self.where = where self.exclude = exclude @@ -590,39 +602,6 @@ def path(self) -> str: return self.where -class PluginFromPackageFinder(PluginFinder): - finder: _PackageFinder - - def __init__(self, finder: _PackageFinder): - self.finder = finder - - def find_plugins(self) -> list[PluginSpec]: - collector = ModuleScanningPluginFinder(self.load_modules()) - return collector.find_plugins() - - def load_modules(self): - for module_name in self.list_module_names(): - try: - yield importlib.import_module(module_name) - except Exception as e: - LOG.error("error importing module %s: %s", module_name, e) - - def list_module_names(self): - # adapted from https://stackoverflow.com/a/54323162/804840 - from pkgutil import iter_modules - - modules = set() - - for pkg in self.finder.find_packages(): - modules.add(pkg) - pkgpath = self.finder.path + os.sep + pkg.replace(".", os.sep) - for info in iter_modules([pkgpath]): - if not info.ispkg: - modules.add(pkg + "." + info.name) - - return modules - - class PackagePathPluginFinder(PluginFromPackageFinder): """ Uses setuptools and pkgutil to find and import modules within a given path and then uses a @@ -631,4 +610,4 @@ class PackagePathPluginFinder(PluginFromPackageFinder): """ def __init__(self, where=".", exclude=(), include=("*",), namespace=True) -> None: - super().__init__(DefaultPackageFinder(where, exclude, include, namespace=namespace)) + super().__init__(SetuptoolsPackageFinder(where, exclude, include, namespace=namespace)) diff --git a/plux/cli/cli.py b/plux/cli/cli.py index 9cb11bd..fcf769b 100644 --- a/plux/cli/cli.py +++ b/plux/cli/cli.py @@ -4,83 +4,101 @@ """ import argparse -import json import logging import os import sys from plux.build import config -from plux.build.setuptools import ( - create_plugin_index_builder, - find_egg_info_dir, - get_distribution_from_workdir, - get_plux_json_path, -) +from plux.build.project import Project + +LOG = logging.getLogger(__name__) + + +def _get_build_backend() -> str | None: + # TODO: should read this from the project configuration instead somehow. + try: + import setuptools # noqa + + return "setuptools" + except ImportError: + pass + + try: + import hatchling # noqa + + return "hatchling" + except ImportError: + pass + + return None + + +def _load_project(args: argparse.Namespace) -> Project: + backend = _get_build_backend() + workdir = args.workdir + + if args.verbose: + print(f"loading project config from {workdir}, determined build backend is: {backend}") + + if backend == "setuptools": + from plux.build.setuptools import SetuptoolsProject + + return SetuptoolsProject(workdir) + elif backend == "hatchling": + raise NotImplementedError("Hatchling is not yet supported as build backend") + else: + raise RuntimeError( + "No supported build backend found. Plux needs either setuptools or hatchling to work." + ) def entrypoints(args: argparse.Namespace): - dist = get_distribution_from_workdir(os.getcwd()) - cfg = config.read_plux_config_from_workdir(os.getcwd()) - cfg.merge( + project = _load_project(args) + project.config = project.config.merge( exclude=args.exclude.split(",") if args.exclude else None, include=args.include.split(",") if args.include else None, ) + cfg = project.config print(f"entry point build mode: {cfg.entrypoint_build_mode.value}") if cfg.entrypoint_build_mode == config.EntrypointBuildMode.BUILD_HOOK: - # TODO: this is code specific to setuptools. integrating additional build tools would require refactoring. - print("discovering plugins ...") - dist.command_options["plugins"] = { - "exclude": ("command line", args.exclude), - "include": ("command line", args.include), - } - dist.run_command("plugins") - - print(f"building {dist.get_name().replace('-', '_')}.egg-info...") - dist.run_command("egg_info") - - print("discovered plugins:") - # print discovered plux plugins - with open(get_plux_json_path(dist)) as fd: - plux_json = json.load(fd) - json.dump(plux_json, sys.stdout, indent=2) - + print("discovering plugins and building entrypoints automatically...") + project.build_entrypoints() elif cfg.entrypoint_build_mode == config.EntrypointBuildMode.MANUAL: path = os.path.join(os.getcwd(), cfg.entrypoint_static_file) - print(f"discovering plugins and writing to {path} ...") - builder = create_plugin_index_builder(cfg, dist, output_format="ini") + builder = project.create_plugin_index_builder() with open(path, "w") as fd: - builder.write(fd) + builder.write(fd, output_format="ini") def discover(args: argparse.Namespace): - dist = get_distribution_from_workdir(os.getcwd()) - cfg = config.read_plux_config_from_workdir(os.getcwd()) - cfg = cfg.merge( + project = _load_project(args) + project.config = project.config.merge( + path=args.path, exclude=args.exclude.split(",") if args.exclude else None, include=args.include.split(",") if args.include else None, - path=args.path, ) - builder = create_plugin_index_builder(cfg, dist, output_format=args.format) - builder.write(args.output) + builder = project.create_plugin_index_builder() + builder.write(fp=args.output, output_format=args.format) def show(args: argparse.Namespace): - egg_info_dir = find_egg_info_dir() - if not egg_info_dir: - print("no *.egg-info directory") + project = _load_project(args) + + try: + entrypoints_file = project.find_entry_point_file() + except FileNotFoundError as e: + print(f"Entrypoints file could not be located: {e}") return - txt = os.path.join(egg_info_dir, "entry_points.txt") - if not os.path.isfile(txt): - print("no entry points to show") + if not entrypoints_file.exists(): + print(f"No entrypoints file found at {entrypoints_file}, nothing to show") return - with open(txt) as fd: - print(fd.read()) + print(entrypoints_file.read_text()) def resolve(args): diff --git a/plux/core/__init__.py b/plux/core/__init__.py index e69de29..9c0fd3b 100644 --- a/plux/core/__init__.py +++ b/plux/core/__init__.py @@ -0,0 +1 @@ +"""Core concepts and API of Plux.""" diff --git a/plux/core/plugin.py b/plux/core/plugin.py index d1ba3bb..8a05d36 100644 --- a/plux/core/plugin.py +++ b/plux/core/plugin.py @@ -91,7 +91,7 @@ def __eq__(self, other): class PluginFinder(abc.ABC): """ Basic abstractions to find plugins, either at build time (e.g., using the PackagePathPluginFinder) or at run time - (e.g., using StevedorePluginFinder that finds plugins from entrypoints) + (e.g., using ``MetadataPluginFinder`` that finds plugins from entrypoints) """ def find_plugins(self) -> list[PluginSpec]: diff --git a/plux/runtime/__init__.py b/plux/runtime/__init__.py index e69de29..cd47129 100644 --- a/plux/runtime/__init__.py +++ b/plux/runtime/__init__.py @@ -0,0 +1,2 @@ +"""This module contains the API and machinery for resolving and loading Plugins from entrypoints at runtime. It is +build-tool independent and only relies on importlib and plux internals.""" diff --git a/pyproject.toml b/pyproject.toml index 1631ab1..ca6a9e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ readme = "README.md" license = "Apache-2.0" classifiers = [ "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", @@ -36,6 +35,15 @@ dev = [ "ruff==0.9.1", ] +[tool.hatch.version] +path = "plux/__init__.py" + +[tool.ruff] +line-length = 110 +target-version = "py310" + +# integrations with setuptools + [project.entry-points."distutils.commands"] plugins = "plux.build.setuptools:plugins" @@ -43,9 +51,3 @@ plugins = "plux.build.setuptools:plugins" # this is actually not a writer, it's a reader :-) "plux.json" = "plux.build.setuptools:load_plux_entrypoints" -[tool.hatch.version] -path = "plux/__init__.py" - -[tool.ruff] -line-length = 110 -target-version = "py310" diff --git a/tests/conftest.py b/tests/conftest.py index 59ae95e..b90ee94 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ import pytest -from plugin.discovery import ModuleScanningPluginFinder +from plux.build.discovery import ModuleScanningPluginFinder from tests.plugins import sample_plugins diff --git a/tests/test_discovery.py b/tests/test_discovery.py index cd06efc..d4324f4 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -1,6 +1,7 @@ import os -from plugin.discovery import ModuleScanningPluginFinder, PackagePathPluginFinder +from plux.build.discovery import ModuleScanningPluginFinder +from plux.build.setuptools import PackagePathPluginFinder from .plugins import sample_plugins diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 772b84a..b1d721b 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -1,7 +1,7 @@ import pytest -from plugin.core import PluginSpec -from plugin.entrypoint import EntryPoint, spec_to_entry_point, to_entry_point_dict +from plux import PluginSpec +from plux.core.entrypoint import EntryPoint, spec_to_entry_point, to_entry_point_dict from .plugins import sample_plugins diff --git a/tests/test_function_plugin.py b/tests/test_function_plugin.py index 0cdd613..79d8d61 100644 --- a/tests/test_function_plugin.py +++ b/tests/test_function_plugin.py @@ -1,7 +1,6 @@ import pytest -from plugin import PluginManager -from plux import PluginDisabled +from plux import PluginDisabled, PluginManager from tests.plugins import sample_plugins diff --git a/tests/test_manager.py b/tests/test_manager.py index 6e43a3e..0ea1436 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -3,7 +3,7 @@ import pytest -from plugin import Plugin, PluginDisabled, PluginFinder, PluginManager, PluginSpec +from plux import Plugin, PluginDisabled, PluginFinder, PluginManager, PluginSpec from plux.runtime.filter import global_plugin_filter diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 9fcc0b6..e6bbc9e 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,5 +1,5 @@ -from plugin import PluginSpec -from plugin.metadata import resolve_distribution_information +from plux import PluginSpec +from plux.runtime.metadata import resolve_distribution_information def test_resolve_distribution_information():