Skip to content

Commit 52aca98

Browse files
committed
add generated docs
1 parent 5494c1b commit 52aca98

13 files changed

Lines changed: 2746 additions & 31 deletions

docs/conf.py

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,30 @@
66
# -- Project information -----------------------------------------------------
77
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
88

9-
project = 'Plux'
10-
copyright = '2026, LocalStack'
11-
author = 'Thomas Rausch'
12-
release = '1.14.0'
9+
project = "Plux"
10+
copyright = "2026, LocalStack"
11+
author = "Thomas Rausch"
12+
release = "1.14.0"
1313

1414
# -- General configuration ---------------------------------------------------
1515
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
1616

17-
extensions = [
18-
'myst_parser'
19-
]
20-
21-
templates_path = ['_templates']
22-
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
17+
extensions = ["myst_parser"]
2318

19+
templates_path = ["_templates"]
20+
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
2421

2522

2623
# -- Options for HTML output -------------------------------------------------
2724
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
2825

29-
html_theme = 'furo'
30-
html_static_path = ['_static']
26+
html_theme = "furo"
27+
html_static_path = ["_static"]
3128
html_title = "Plux documentation"
3229

3330
html_theme_options = {
3431
"top_of_page_buttons": ["view", "edit"],
3532
"source_repository": "https://github.com/localstack/plux/",
3633
"source_branch": "main",
3734
"source_directory": "docs/",
38-
"footer_icons": [
39-
{
40-
"name": "GitHub",
41-
"url": "https://github.com/localstack/plux",
42-
"html": """
43-
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
44-
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69>
45-
</svg>
46-
""",
47-
"class": "",
48-
},
49-
],
5035
}

docs/index.rst

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,39 @@
33
You can adapt this file completely to your liking, but it should at least
44
contain the root `toctree` directive.
55
6-
Plux documentation
7-
==================
6+
Plux - Dynamic Code Loading Framework
7+
=====================================
88

9-
Add your content using ``reStructuredText`` syntax. See the
10-
`reStructuredText <https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html>`_
11-
documentation for details.
9+
Plux is the dynamic code loading framework used in `LocalStack <https://github.com/localstack/localstack>`_. It builds a higher-level plugin mechanism around `Python's entry point mechanism <https://packaging.python.org/specifications/entry-points/>`_.
1210

11+
Plux provides tools to load plugins from entry points at run time, and to discover entry points from plugins at build time (so you don't have to declare entry points statically in your ``setup.cfg`` or ``pyproject.toml``).
12+
13+
.. image:: plux-architecture.png
14+
:alt: Plux Architecture
15+
:align: center
16+
17+
.. toctree::
18+
:maxdepth: 1
19+
:caption: User Guide
20+
21+
user_guide/quickstart
22+
user_guide/defining_loading_plugins
23+
user_guide/plugin_manager
24+
user_guide/filters
25+
user_guide/lifecycle_listener
26+
user_guide/build_integration
27+
user_guide/cli
28+
29+
.. toctree::
30+
:maxdepth: 1
31+
:caption: Reference
32+
33+
reference/build_discovery
34+
reference/runtime_discovery
35+
reference/setuptools_integration
1336

1437
.. toctree::
15-
:maxdepth: 2
16-
:caption: Contents:
38+
:maxdepth: 1
39+
:caption: Tutorials
1740

41+
tutorials/index

docs/reference/build_discovery.rst

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
Plugin Discovery at Build Time
2+
===========================
3+
4+
This reference guide explains how plux discovers plugins at build time. Understanding this process is helpful for troubleshooting plugin discovery issues and for extending plux to support custom plugin discovery mechanisms.
5+
6+
Overview
7+
-------
8+
9+
At build time, plux scans your project's source code to find plugins. This is done through a series of abstractions that handle different aspects of the discovery process:
10+
11+
1. **PackageFinder**: Finds Python packages to scan
12+
2. **PluginFinder**: Finds plugins within those packages
13+
3. **PluginSpecResolver**: Resolves plugin specifications from various sources
14+
15+
The Discovery Process
16+
------------------
17+
18+
The build-time discovery process follows these steps:
19+
20+
1. A **PackageFinder** implementation (e.g., ``DistributionPackageFinder``) identifies Python packages to scan
21+
2. A **PluginFromPackageFinder** uses the PackageFinder to load modules from those packages
22+
3. A **ModuleScanningPluginFinder** scans the loaded modules for plugin specifications
23+
4. A **PluginSpecResolver** resolves plugin specifications from various sources (classes, functions, etc.)
24+
5. The discovered plugin specifications are written to a plugin index file (``plux.json``)
25+
6. The plugin index is used to generate entry points in ``entry_points.txt``
26+
27+
Package Discovery
28+
--------------
29+
30+
The ``PackageFinder`` abstraction is responsible for finding Python packages to scan for plugins:
31+
32+
.. code-block:: python
33+
34+
class PackageFinder:
35+
def find_packages(self) -> t.Iterable[str]:
36+
"""
37+
Returns an Iterable of Python packages. Each item is a string-representation
38+
of a Python package (for example, ``plux.core``, ``myproject.mypackage.utils``, ...)
39+
"""
40+
raise NotImplementedError
41+
42+
@property
43+
def path(self) -> str:
44+
"""
45+
The root file path under which the packages are located.
46+
"""
47+
raise NotImplementedError
48+
49+
Plux provides several implementations:
50+
51+
1. **DistributionPackageFinder**: Uses setuptools to find packages in a distribution
52+
2. **SetuptoolsPackageFinder**: Uses setuptools directly to find packages
53+
54+
Module Loading
55+
-----------
56+
57+
The ``PluginFromPackageFinder`` class loads modules from the packages found by the ``PackageFinder``:
58+
59+
.. code-block:: python
60+
61+
class PluginFromPackageFinder(PluginFinder):
62+
def __init__(self, finder: PackageFinder):
63+
self.finder = finder
64+
65+
def find_plugins(self) -> list[PluginSpec]:
66+
collector = ModuleScanningPluginFinder(self._load_modules())
67+
return collector.find_plugins()
68+
69+
def _load_modules(self) -> t.Generator[ModuleType, None, None]:
70+
# Load modules from packages
71+
...
72+
73+
This class:
74+
75+
1. Gets a list of package names from the ``PackageFinder``
76+
2. Converts package names to module names
77+
3. Imports each module using ``importlib.import_module()``
78+
4. Passes the loaded modules to a ``ModuleScanningPluginFinder``
79+
80+
Module Scanning
81+
------------
82+
83+
The ``ModuleScanningPluginFinder`` class scans loaded modules for plugin specifications:
84+
85+
.. code-block:: python
86+
87+
class ModuleScanningPluginFinder(PluginFinder):
88+
def __init__(self, modules: t.Iterable[ModuleType], resolver: PluginSpecResolver = None) -> None:
89+
self.modules = modules
90+
self.resolver = resolver or PluginSpecResolver()
91+
92+
def find_plugins(self) -> list[PluginSpec]:
93+
plugins = list()
94+
95+
for module in self.modules:
96+
members = inspect.getmembers(module)
97+
98+
for member in members:
99+
if type(member) is tuple:
100+
try:
101+
spec = self.resolver.resolve(member[1])
102+
plugins.append(spec)
103+
except Exception:
104+
pass
105+
106+
return plugins
107+
108+
This class:
109+
110+
1. Iterates through each module
111+
2. Gets all members of the module using ``inspect.getmembers()``
112+
3. Tries to resolve each member as a plugin specification
113+
4. Collects all successfully resolved plugin specifications
114+
115+
Plugin Specification Resolution
116+
----------------------------
117+
118+
The ``PluginSpecResolver`` class resolves plugin specifications from various sources:
119+
120+
.. code-block:: python
121+
122+
class PluginSpecResolver:
123+
def resolve(self, source: t.Any) -> PluginSpec:
124+
if isinstance(source, PluginSpec):
125+
return source
126+
127+
if inspect.isclass(source):
128+
if issubclass(source, Plugin):
129+
return PluginSpec(source.namespace, source.name, source)
130+
131+
if inspect.isfunction(source):
132+
spec = getattr(source, "__pluginspec__", None)
133+
if spec and isinstance(spec, PluginSpec):
134+
return spec
135+
136+
raise ValueError("cannot resolve plugin specification from %s" % source)
137+
138+
This class can resolve plugin specifications from:
139+
140+
1. Existing ``PluginSpec`` instances
141+
2. Plugin classes (subclasses of ``Plugin`` with ``namespace`` and ``name`` attributes)
142+
3. Functions decorated with ``@plugin`` (which have a ``__pluginspec__`` attribute)
143+
144+
Filtering Packages
145+
---------------
146+
147+
Plux provides filtering capabilities to include or exclude specific packages during discovery:
148+
149+
.. code-block:: python
150+
151+
class Filter:
152+
def __init__(self, patterns: t.Iterable[str]):
153+
self._patterns = patterns
154+
155+
def __call__(self, item: str):
156+
return any(fnmatchcase(item, pat) for pat in self._patterns)
157+
158+
Filters can be configured in ``pyproject.toml`` or via command-line arguments:
159+
160+
.. code-block:: toml
161+
162+
[tool.plux]
163+
exclude = ["**/database/alembic*", "tests*"]
164+
include = ["myapp/plugins*"]
165+
166+
Plugin Index Building
167+
------------------
168+
169+
The ``PluginIndexBuilder`` class builds a plugin index from discovered plugins:
170+
171+
.. code-block:: python
172+
173+
class PluginIndexBuilder:
174+
def __init__(self, finder: PluginFinder):
175+
self.finder = finder
176+
177+
def write(self, fp, output_format="json") -> EntryPointDict:
178+
plugins = self.finder.find_plugins()
179+
entry_points = discover_entry_points(plugins)
180+
181+
if output_format == "json":
182+
json.dump(entry_points, fp, indent=2)
183+
elif output_format == "ini":
184+
write_ini(entry_points, fp)
185+
186+
return entry_points
187+
188+
This class:
189+
190+
1. Uses a ``PluginFinder`` to discover plugins
191+
2. Converts the discovered plugins to entry points
192+
3. Writes the entry points to a file in the specified format (JSON or INI)
193+
194+
Entry Point Generation
195+
-------------------
196+
197+
The final step is generating entry points from the plugin index:
198+
199+
1. In **build-hooks mode**, plux hooks into the setuptools build process:
200+
- The ``plugins`` command writes discovered plugins to ``plux.json``
201+
- The ``egg_info`` command reads ``plux.json`` and updates ``entry_points.txt``
202+
203+
2. In **manual mode**, plux writes entry points to ``plux.ini``, which you include in your build configuration:
204+
205+
.. code-block:: toml
206+
207+
[project]
208+
dynamic = ["entry-points"]
209+
210+
[tool.setuptools.dynamic]
211+
entry-points = {file = ["plux.ini"]}
212+
213+
Extending Plugin Discovery
214+
-----------------------
215+
216+
You can extend plux's plugin discovery mechanism by implementing custom finders:
217+
218+
Custom Package Finder
219+
~~~~~~~~~~~~~~~~~~
220+
221+
To customize how packages are discovered:
222+
223+
.. code-block:: python
224+
225+
from plux.build.discovery import PackageFinder
226+
227+
class MyPackageFinder(PackageFinder):
228+
def __init__(self, custom_packages):
229+
self.custom_packages = custom_packages
230+
231+
def find_packages(self) -> t.Iterable[str]:
232+
return self.custom_packages
233+
234+
@property
235+
def path(self) -> str:
236+
return "."
237+
238+
Custom Plugin Finder
239+
~~~~~~~~~~~~~~~~~
240+
241+
To customize how plugins are discovered:
242+
243+
.. code-block:: python
244+
245+
from plux import PluginFinder, PluginSpec
246+
247+
class MyPluginFinder(PluginFinder):
248+
def find_plugins(self) -> list[PluginSpec]:
249+
# Custom plugin discovery logic
250+
return discovered_plugins
251+
252+
Troubleshooting
253+
------------
254+
255+
Common issues with build-time plugin discovery:
256+
257+
1. **Plugins not being discovered**:
258+
- Check that your plugins correctly define ``namespace`` and ``name`` attributes
259+
- Verify that your include/exclude patterns aren't filtering out your plugins
260+
- Try running with verbose logging: ``python -m plux entrypoints -v``
261+
262+
2. **Import errors during discovery**:
263+
- Plux imports modules to discover plugins, which can cause issues if modules have side effects
264+
- Use exclude patterns to skip problematic modules: ``exclude = ["**/problematic_module*"]``
265+
266+
3. **Performance issues**:
267+
- Scanning large codebases can be slow
268+
- Use include patterns to focus on specific packages: ``include = ["myapp/plugins*"]``
269+
270+
Summary
271+
------
272+
273+
Plux's build-time plugin discovery process involves:
274+
275+
1. Finding Python packages to scan
276+
2. Loading modules from those packages
277+
3. Scanning modules for plugin specifications
278+
4. Resolving plugin specifications from various sources
279+
5. Building a plugin index
280+
6. Generating entry points from the plugin index
281+
282+
This process is highly customizable through the ``PackageFinder`` and ``PluginFinder`` abstractions, and can be configured using include/exclude patterns in ``pyproject.toml`` or via command-line arguments.

0 commit comments

Comments
 (0)