Skip to content

Replace pluggy with an entry-point registry and reshape LanguageRegistration#69

Open
janrito wants to merge 4 commits into
feat/lang-test-harnessfrom
feat/lang-plugin-architecture
Open

Replace pluggy with an entry-point registry and reshape LanguageRegistration#69
janrito wants to merge 4 commits into
feat/lang-test-harnessfrom
feat/lang-plugin-architecture

Conversation

@janrito

@janrito janrito commented Jul 8, 2026

Copy link
Copy Markdown
Owner

This removes pluggy from the plugin system and replaces it with an
entry-point registry, and changes how a language plugin is declared: a
data-only LanguageRegistration value with decorator-based overrides for
custom extraction.

pluggy is a multi-dispatch hook framework. Languages are rbtr's only
plugin type, and every file maps to exactly one language by id, so the
dispatch is single. pluggy adds no value here and adds a cost: entry
points had to resolve to a plugin instance (a class with @hookimpl plus
plugin = XPlugin()), which is easy to get wrong. tree-sitter uses no
plugin framework — it passes objects directly — and this change follows
that approach.

Plugins are discovered from the rbtr.languages entry-point group with
importlib.metadata. Each entry point resolves to a module-level
LanguageRegistration named by its language id
(rbtr_lang_bash.plugin:bash): no class, no hook markers, no instance. A
duplicate id raises, because two packages claiming one id is a bug and
entry-point order across distributions is not deterministic. A plugin
that fails to import is logged and skipped, so one broken third-party
plugin does not stop the rest.

The registration constructor is now pure data. Custom name, scope,
import, or chunker logic attaches through decorator methods
(@bash.name_extractor) shaped like a pydantic WrapValidator: the
override receives the built-in resolver as its first argument and calls
it to delegate. extract_symbols takes the registration and asks it to
resolve names, scopes, and imports, instead of the orchestrator reading
private fields. Behaviour stays in injected callables rather than a base
class, following the abstraction-design guidance against template-method
hierarchies. The trade-off: with one slot per hook, every
name/scope/import override takes the resolver argument even when it does
not delegate.

# BEFORE (pluggy)
from rbtr.languages.hookspec import LanguageRegistration, hookimpl, resolve_name

class BashPlugin:
    @hookimpl
    def rbtr_register_languages(self) -> list[LanguageRegistration]:
        return [LanguageRegistration(id="bash", ..., name_extractor=_strip_alias_eq)]

PLUGIN = BashPlugin()          # entry point: "rbtr_lang_bash.plugin:PLUGIN"
# AFTER (registry)
from rbtr.languages.registration import LanguageRegistration, NameResolver

bash = LanguageRegistration(id="bash", ...)   # entry point: "rbtr_lang_bash.plugin:bash"

@bash.name_extractor
def _strip_alias_eq(resolver: NameResolver, capture_name, node, captures) -> str:
    return resolver(capture_name, node, captures).rstrip("=")

The authoring documentation is updated to the new API and packaged model:
the rbtr-languages skill, and an inline docstring on every
LanguageRegistration field.

No behaviour change: a language extracts what it did before. The registry
loader is tested for the cases that matter — a duplicate id raises, and a
broken plugin is skipped without taking the others down.

janrito added 4 commits July 8, 2026 23:51
…tion

Drop pluggy (languages are single-dispatch by id, so a multi-dispatch
hook framework earns nothing): discover plugins from the
`rbtr.languages` entry-point group via `importlib.metadata`, each
resolving to a module-level `LanguageRegistration`. Make `load_query`
public and rename `hookspec` to `registration`. The constructor is now
pure data; extraction overrides attach via wrap-style decorator methods
(the built-in resolver is passed in), and `extract_symbols` takes the
registration and delegates resolution to it.
The bash and sql package pyprojects still described the entry-point
value as a plugin instance; it now resolves to a LanguageRegistration,
matching core's comment.
The skill taught removed Phase-9 symbols (_QUERY constants, resolve_name/
resolve_scope/build_import_from_captures, and the old non-wrap
name_extractor(capture_name, ...) signature) and the pre-packaging model
(a plugin as a file under rbtr/languages/, samples under
tests/languages/samples/). Update it to wrap-style resolvers (default_*,
resolver-first, attached via @reg decorator methods), entry-point
discovery (no pluggy), and the packaged layout (rbtr_lang_<x> package
with co-located .scm queries and per-package tests/samples/).
The class docstring examples passed chunker= and import_extractor= as
constructor arguments, contradicting the adjacent 'attach via decorator
methods' prose. Fix them to the decorator form, drop the mixed
Attributes: block in favour of an inline per-field docstring on every
field (removing the class_scope_types duplication), and flesh out the
under-documented import-resolution fields (import_targets, source_roots,
path_substitutions, index_files). Also fix a stale samples path in the
module docstring.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant