Skip to content

Commit 4e56112

Browse files
committed
Add Exclude option when building entrypoints
1 parent 4ffe5ba commit 4e56112

8 files changed

Lines changed: 152 additions & 7 deletions

File tree

plugin/entrypoint.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Deprecated bindings, use plux imports instead, but you shouldn't use the internals in the first place.
33
"""
4+
45
from plux.build.setuptools import find_plugins
56
from plux.core.entrypoint import (
67
EntryPoint,

plux/build/setuptools.py

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import re
99
import shutil
1010
import sys
11+
import tomllib
1112
import typing as t
13+
from fnmatch import fnmatchcase
1214
from pathlib import Path
1315

1416
import setuptools
@@ -39,20 +41,31 @@ def _ensure_dist_info(self, *args, **kwargs):
3941
class plugins(InfoCommon, setuptools.Command):
4042
description = "Discover plux plugins and store them in .egg_info"
4143

42-
user_options = [
43-
# TODO
44+
user_options: t.ClassVar[list[tuple[str, str, str]]] = [
45+
('exclude=', 'e', "exclude those files when discovering plugins"),
46+
# TODO: add more
4447
]
4548

4649
egg_info: str
4750

4851
def initialize_options(self) -> None:
4952
self.plux_json_path = None
53+
self.exclude = None
5054

5155
def finalize_options(self) -> None:
5256
self.plux_json_path = get_plux_json_path(self.distribution)
57+
self.ensure_string_list('exclude')
58+
if self.exclude is None:
59+
self.exclude = []
60+
61+
project_config = read_configuration(self.distribution)
62+
file_exclude = project_config.get("exclude")
63+
if file_exclude:
64+
self.exclude = set(self.exclude) | set(file_exclude)
65+
self.exclude = [_path_to_module(item) for item in self.exclude]
5366

5467
def run(self) -> None:
55-
plugin_finder = PluginFromPackageFinder(DistributionPackageFinder(self.distribution))
68+
plugin_finder = PluginFromPackageFinder(DistributionPackageFinder(self.distribution, exclude=self.exclude))
5669
ep = discover_entry_points(plugin_finder)
5770

5871
self.debug_print(f"writing discovered plugins into {self.plux_json_path}")
@@ -191,6 +204,20 @@ def get_plux_json_path(distribution):
191204
return os.path.join(egg_info_dir, "plux.json")
192205

193206

207+
def read_configuration(distribution) -> dict:
208+
dirs = distribution.package_dir
209+
pyproject_base = (dirs or {}).get("", os.curdir)
210+
pyproject_file = os.path.join(pyproject_base, "pyproject.toml")
211+
if not os.path.exists(pyproject_file):
212+
return {}
213+
214+
with open(pyproject_file, "rb") as file:
215+
pyproject_config = tomllib.load(file)
216+
217+
tool_table = pyproject_config.get("tool", {})
218+
return tool_table.get("plux", {})
219+
220+
194221
def update_entrypoints(distribution, ep: EntryPointDict):
195222
if distribution.entry_points is None:
196223
distribution.entry_points = {}
@@ -375,6 +402,27 @@ def _to_filename(name):
375402
return name.replace("-", "_")
376403

377404

405+
def _path_to_module(path):
406+
"""
407+
Convert a path to a Python module to its module representation
408+
Example: plux/core/test -> plux.core.test
409+
"""
410+
return path.strip("/").replace("/", ".")
411+
412+
413+
class _Filter:
414+
"""
415+
Given a list of patterns, create a callable that will be true only if
416+
the input matches at least one of the patterns.
417+
This is from `setuptools.discovery._Filter`
418+
"""
419+
def __init__(self, patterns: t.Iterable[str]):
420+
self._patterns = patterns
421+
422+
def __call__(self, item: str):
423+
return any(fnmatchcase(item, pat) for pat in self._patterns)
424+
425+
378426
class _PackageFinder:
379427
"""
380428
Generate a list of Python packages. How these are generated depends on the implementation.
@@ -397,11 +445,12 @@ class DistributionPackageFinder(_PackageFinder):
397445
correctly if configured.
398446
"""
399447

400-
def __init__(self, distribution: Distribution):
448+
def __init__(self, distribution: Distribution, exclude: t.Optional[t.Iterable[str]] = None):
401449
self.distribution = distribution
450+
self.exclude = _Filter(exclude or [])
402451

403452
def find_packages(self) -> t.Iterable[str]:
404-
return self.distribution.packages
453+
return self.filter_packages(self.distribution.packages)
405454

406455
@property
407456
def path(self) -> str:
@@ -415,6 +464,9 @@ def path(self) -> str:
415464
where = "."
416465
return where
417466

467+
def filter_packages(self, packages: t.Iterable[str]) -> t.Iterable[str]:
468+
return [item for item in packages if not self.exclude(item)]
469+
418470

419471
class DefaultPackageFinder(_PackageFinder):
420472
def __init__(self, where=".", exclude=(), include=("*",), namespace=True) -> None:

plux/cli/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def entrypoints(args):
2020
dist = get_distribution_from_workdir(os.getcwd())
2121

2222
print("discovering plugins ...")
23+
dist.command_options["plugins"] = {"exclude": ("command line", args.exclude)}
2324
dist.run_command("plugins")
2425

2526
print(f"building {dist.get_name().replace('-', '_')}.egg-info...")
@@ -82,6 +83,7 @@ def main(argv=None):
8283
generate_parser = subparsers.add_parser(
8384
"entrypoints", help="Discover plugins and generate entry points"
8485
)
86+
generate_parser.add_argument("-e", "--exclude", help="path(s) to exclude")
8587
generate_parser.set_defaults(func=entrypoints)
8688

8789
# Subparser for the 'discover' subcommand

tests/cli/projects/pyproject/mysrc/subpkg/__init__.py

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from plugin import Plugin
2+
3+
4+
class MyNestedPlugin(Plugin):
5+
namespace = "plux.test.plugins"
6+
name = "mynestedplugin"

tests/cli/projects/setupcfg/mysrc/subpkg/__init__.py

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from plugin import Plugin
2+
3+
4+
class MyNestedPlugin(Plugin):
5+
namespace = "plux.test.plugins"
6+
name = "mynestedplugin"

tests/cli/test_entrypoints.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import os.path
2+
import shutil
23
import sys
3-
from pathlib import Path
44

55
import pytest
66

@@ -26,10 +26,88 @@ def test_entrypoints(project_name):
2626

2727
with open(os.path.join(project, "test_project.egg-info", "entry_points.txt"), "r") as f:
2828
lines = [line.strip() for line in f.readlines() if line.strip()]
29-
assert lines == ["[plux.test.plugins]", "myplugin = mysrc.plugins:MyPlugin"]
29+
assert lines == [
30+
"[plux.test.plugins]",
31+
"mynestedplugin = mysrc.subpkg.plugins:MyNestedPlugin",
32+
"myplugin = mysrc.plugins:MyPlugin",
33+
]
3034

3135
# make sure that SOURCES.txt contain no absolute paths
3236
with open(os.path.join(project, "test_project.egg-info", "SOURCES.txt"), "r") as f:
3337
lines = [line.strip() for line in f.readlines() if line.strip()]
3438
for line in lines:
3539
assert not line.startswith("/")
40+
41+
42+
@pytest.mark.parametrize("project_name", ["pyproject", "setupcfg"])
43+
def test_entrypoints_exclude(project_name):
44+
if project_name == "pyproject" and sys.version_info < (3, 10):
45+
pytest.xfail("reading pyproject.toml requires Python 3.10 or above")
46+
47+
from plux.__main__ import main
48+
49+
project = os.path.join(os.path.dirname(__file__), "projects", project_name)
50+
os.chdir(project)
51+
52+
sys.path.append(project)
53+
try:
54+
try:
55+
main(["--workdir", project, "entrypoints", "--exclude", "**/subpkg*"])
56+
except SystemExit:
57+
pass
58+
finally:
59+
sys.path.remove(project)
60+
61+
with open(os.path.join(project, "test_project.egg-info", "entry_points.txt"), "r") as f:
62+
lines = [line.strip() for line in f.readlines() if line.strip()]
63+
assert lines == [
64+
"[plux.test.plugins]",
65+
"myplugin = mysrc.plugins:MyPlugin",
66+
]
67+
68+
# make sure that SOURCES.txt contain no absolute paths
69+
with open(os.path.join(project, "test_project.egg-info", "SOURCES.txt"), "r") as f:
70+
lines = [line.strip() for line in f.readlines() if line.strip()]
71+
for line in lines:
72+
assert not line.startswith("/")
73+
74+
75+
def test_entrypoints_exclude_from_pyproject_config(tmp_path):
76+
if sys.version_info < (3, 10):
77+
pytest.xfail("reading pyproject.toml requires Python 3.10 or above")
78+
79+
from plux.__main__ import main
80+
81+
src_project = os.path.join(os.path.dirname(__file__), "projects", "pyproject")
82+
dest_project = os.path.join(str(tmp_path), "pyproject")
83+
84+
shutil.copytree(src_project, dest_project)
85+
86+
pyproject_toml_path = os.path.join(dest_project, "pyproject.toml")
87+
88+
with open(pyproject_toml_path, "a") as fp:
89+
fp.write('\n[tool.plux]\nexclude = ["**subpkg*"]\n')
90+
91+
os.chdir(dest_project)
92+
93+
sys.path.append(dest_project)
94+
try:
95+
try:
96+
main(["--workdir", dest_project, "entrypoints"])
97+
except SystemExit:
98+
pass
99+
finally:
100+
sys.path.remove(dest_project)
101+
102+
with open(os.path.join(dest_project, "test_project.egg-info", "entry_points.txt"), "r") as f:
103+
lines = [line.strip() for line in f.readlines() if line.strip()]
104+
assert lines == [
105+
"[plux.test.plugins]",
106+
"myplugin = mysrc.plugins:MyPlugin",
107+
]
108+
109+
# make sure that SOURCES.txt contain no absolute paths
110+
with open(os.path.join(dest_project, "test_project.egg-info", "SOURCES.txt"), "r") as f:
111+
lines = [line.strip() for line in f.readlines() if line.strip()]
112+
for line in lines:
113+
assert not line.startswith("/")

0 commit comments

Comments
 (0)