Skip to content

[IMP] plugin: search, list and inspect odev plugins - #174

Merged
brinkflew merged 4 commits into
betafrom
beta-plugin-discovery-avs
Aug 27, 2026
Merged

[IMP] plugin: search, list and inspect odev plugins#174
brinkflew merged 4 commits into
betafrom
beta-plugin-discovery-avs

Conversation

@brinkflew

Copy link
Copy Markdown
Contributor

Description

odev plugin could only enable, disable or show a plugin whose name you already knew. This PR adds discovery to the command, and makes --show the detail view for every plugin odev can know about.

  • odev plugin --search [terms] — searches GitHub for published plugins, keeping only repositories exposing a valid manifest at their root. The odev keyword alone is unusable (it collides with unrelated repositories, mostly Turkish ödev), so repositories are matched on both odev and plugin in name, description or topics; on the current index that returns 13 real plugins out of 14 hits. Archived repositories and the template repository, which cannot be installed, are left out. --limit caps how many repositories are inspected (20 by default), bounding the cost to 1 + N API requests.
  • odev plugin --list — every plugin available locally with its state: enabled, disabled (downloaded but not linked), missing (link gone) or shadowed. That last state was previously invisible: two enabled plugins forked from one another map to the same module name and only one is ever loaded — which is the case today for odoo-odev/odev-plugin-editor-vscode and its avanserv fork.
  • odev plugin --show — now built on the same discovery, so it reports states consistently with --list. A plugin that is not on the machine is looked up on GitHub, so uninstalled and never-downloaded plugins are described too; plugins present locally are read from disk and never trigger a request. Failing to reach GitHub falls back to the local information instead of raising. Without an argument, --show details every local plugin rather than the enabled ones only.

Searching and listing never install anything — installing remains odev plugin --enable <organization>/<repository>.

Supporting changes:

  • parse_plugin_manifest() (odev/common/odev.py) — reads a manifest with ast, taking only the module docstring and top-level literal assignments. Manifests are otherwise loaded through exec_module, which is fine for a repository the user explicitly installed but unacceptable for arbitrary search results. A top-level __version__ string is what identifies a repository as an odev plugin. plugin_module_name() is extracted at the same time, replacing the same expression repeated in four places.
  • GithubConnector (odev/common/connectors/git.py) — GitConnector requires an organization/repository at construction, so there was nowhere for a search to live. The API connection concern is extracted into a new base class (a pure move of the token, connection and authentication members; GitConnector inherits from it and keeps its public API), which gains search_repositories(), get_repository() and get_repository_file(). All three return None rather than raising when a result is missing or unreadable.

Also fixes --show <organization>/<repository>, which always reported a plugin as disabled: the name was stripped of its organization before being compared to the enabled plugins, which are stored fully qualified. A repository name on its own is now accepted too, as long as it is not ambiguous.

Testing

  • Full test suite: 165 passed (143 → 157 → 165; 18 new tests covering the manifest parser, the connector and the three command modes), py3.14, local PostgreSQL.
  • pre-commit run --all-files clean; basedpyright reports no new error against the baseline.
  • Exercised against the real GitHub API from a live checkout: --search, --search ai, --search with no result, --list, and --show on an enabled plugin (no request), the template repository (template warning, no install hint), an archived plugin (archived warning), a non-existent repository, the shadowed avanserv fork, a bare unknown name, and with no argument.
  • A manifest containing os.system(...) and raise SystemExit(1) at module level is parsed with no side effect (covered by a unit test).

Compliance

  • I have read the contribution guide
  • I made sure the documentation is up-to-date both in doctrings and the docs directory
  • I have added or modified unit tests where necessary
  • I have added new libraries to the requirements.txt file, if any
  • I have incremented the version number according the versioning guide
  • The PR contains my changes only and no other external commit

🤖 Generated with Claude Code

https://claude.ai/code/session_01H9M1zJCzxpg35ijsDcPEPA

Reading a plugin manifest went through `exec_module`, which is fine for a
repository the user explicitly installed but unacceptable for a manifest
coming from an arbitrary GitHub repository.

Add `parse_plugin_manifest()`, which extracts the name, version, description
and dependencies of a plugin from the source of its manifest using `ast`,
reading only the module docstring and top-level literal assignments. A source
that does not declare a top-level `__version__` string is not a plugin
manifest, which is the test used to tell odev plugins apart from any other
repository.

Also extract `plugin_module_name()`, the expression converting a plugin name
to the module it is linked to under the plugins directory, repeated in four
places.

Claude-Session: https://claude.ai/code/session_01H9M1zJCzxpg35ijsDcPEPA
`GitConnector` requires an `organization/repository` name at construction, so
there was no way to talk to the GitHub API without a repository in hand.

Extract the API connection concern into a new `GithubConnector` base class,
a pure move of the token, connection, authentication and (dis)connection
members; `GitConnector` inherits from it and keeps its public API unchanged.

The new connector exposes three operations on top of it:

- `search_repositories()` searches GitHub, capped to a maximum number of
  results to stay within the rate limits of the search API,
- `get_repository()` fetches a single repository by its full name,
- `get_repository_file()` reads a file from a remote repository without
  cloning it.

All three report a missing or unreadable result as `None` rather than raising,
so callers can degrade gracefully when GitHub cannot be reached.

Claude-Session: https://claude.ai/code/session_01H9M1zJCzxpg35ijsDcPEPA
The `plugin` command could only enable, disable or show a plugin whose name
you already knew. There was no way to discover which plugins exist, and no
way to see which ones are on the machine: `--show` without an argument only
covers enabled plugins, and a plugin that was disabled keeps its clone under
the repositories directory without ever being mentioned again.

Add two modes to the command:

- `--search [terms]` looks for plugins published on GitHub, keeping only the
  repositories exposing a valid manifest at their root. The `odev` keyword
  alone is far too noisy to be usable, so repositories are matched on both
  `odev` and `plugin` in their name, description or topics. Archived
  repositories and the template repository, which cannot be installed, are
  left out. `--limit` caps how many repositories are inspected.
- `--list` displays every plugin available locally with its state: `enabled`,
  `disabled`, `missing` when the link under the plugins directory is gone, or
  `shadowed` when another plugin already uses its module name. That last
  state was previously invisible although two enabled plugins forked from one
  another do collide, only one of them ever being loaded.

`--show` builds on the same discovery and reports the state of a plugin
consistently with `--list`. A plugin that is not available locally is looked
up on GitHub, so uninstalled and never-downloaded plugins are described too;
plugins present on the machine are read from disk and never trigger a request.
Failing to reach GitHub falls back to the information available locally
instead of raising. Without an argument, `--show` details every plugin
available locally rather than the enabled ones only.

Searching and listing never install anything: installing remains
`odev plugin --enable <organization>/<repository>`.

Fix `--show <organization>/<repository>`, which always reported a plugin as
disabled: the name was stripped of its organization before being compared to
the enabled plugins, which are stored fully qualified. The repository name
alone is now accepted as well, as long as it is not ambiguous.

Claude-Session: https://claude.ai/code/session_01H9M1zJCzxpg35ijsDcPEPA
@brinkflew
brinkflew requested a review from sea-odoo July 26, 2026 14:08
Base automatically changed from beta to main July 26, 2026 14:35
sea-odoo
sea-odoo previously approved these changes Jul 30, 2026
@sea-odoo

Copy link
Copy Markdown
Contributor

@brinkflew doesn't target beta is it intended ?

@brinkflew
brinkflew changed the base branch from main to beta July 30, 2026 08:57
@brinkflew
brinkflew dismissed sea-odoo’s stale review July 30, 2026 08:57

The base branch was changed.

@brinkflew

Copy link
Copy Markdown
Contributor Author

Fixed

sea-odoo
sea-odoo previously approved these changes Jul 30, 2026
Merging beta brings in the deferred imports of #176, which moved the GitHub client out of the
module scope. The GithubConnector extracted here keeps them inside the methods that need them,
and the connectors package registers it with the lazy exports instead of importing it eagerly.

Also bumps the version to 4.31.0, one increment above beta.
brinkflew added a commit that referenced this pull request Aug 6, 2026
…e ones

#174 added a TestGithubConnectorRepositories class to this module, and both branches narrowed
its imports to what they needed. Both sets of classes are kept and the imports are the union of
the two.

Also bumps the version to 4.31.3, one increment above the base branch.
@brinkflew

Copy link
Copy Markdown
Contributor Author

Merge order: 1st of 4

Prerequisites: none. This PR targets beta and is the head of the queue.

beta has moved to 4.30.4 since this branch was opened — #135, #177, #180 and #176 are already merged. The latest commit merges beta in, resolves the conflicts it brought, and bumps the version to 4.31.0.

What the merge commit resolves

#176 moved the GitHub API client out of the module scope so importing odev.common.connectors no longer drags it in. This branch extracts a GithubConnector base class that uses that client. Taking either side of the conflict alone would have dropped one of the two changes, so they are combined:

  • the auth/connect/disconnect methods relocated here keep the deferred imports inside their bodies
  • Repository and Github moved to TYPE_CHECKING, with quoted annotations and cast("Github", ...) — the module has no from __future__ import annotations
  • connectors/__init__.py registers GithubConnector with the lazy exports instead of importing it eagerly

Verified: GitConnector still subclasses GithubConnector, and github is not in sys.modules after importing the connectors package.

Remaining queue

Order PR Branch
1 #174 (this one) beta-plugin-discovery-avs
2 #175 avs-update-warning
3 #178 avs-tests-coverage-and-fixes
4 #179 avs-repository-path

Each of the three below targets its predecessor, so GitHub retargets them to beta automatically as this one lands.

Warning

GitHub Actions has not run in this repository since 2026-07-29, so the checks here are stale. The full suite was run locally against the exact merged tree of all four: 357 passed, pre-commit clean.

@brinkflew brinkflew closed this Aug 27, 2026
@brinkflew brinkflew reopened this Aug 27, 2026
@brinkflew
brinkflew merged commit 23ccf41 into main Aug 27, 2026
6 checks passed
@brinkflew
brinkflew deleted the beta-plugin-discovery-avs branch August 27, 2026 13:57
brinkflew added a commit that referenced this pull request Aug 27, 2026
## Description

`odev plugin` could only enable, disable or show a plugin whose name you already knew. This PR adds discovery to the command, and makes `--show` the detail view for every plugin odev can know about.

- **`odev plugin --search [terms]`** — searches GitHub for published plugins, keeping only repositories exposing a valid manifest at their root. The `odev` keyword alone is unusable (it collides with unrelated repositories, mostly Turkish *ödev*), so repositories are matched on both `odev` and `plugin` in name, description or topics; on the current index that returns 13 real plugins out of 14 hits. Archived repositories and [the template repository](https://github.com/odoo-odev/odev-plugin-template), which cannot be installed, are left out. `--limit` caps how many repositories are inspected (20 by default), bounding the cost to `1 + N` API requests.
- **`odev plugin --list`** — every plugin available locally with its state: `enabled`, `disabled` (downloaded but not linked), `missing` (link gone) or `shadowed`. That last state was previously invisible: two enabled plugins forked from one another map to the same module name and only one is ever loaded — which is the case today for `odoo-odev/odev-plugin-editor-vscode` and its `avanserv` fork.
- **`odev plugin --show`** — now built on the same discovery, so it reports states consistently with `--list`. A plugin that is not on the machine is looked up on GitHub, so uninstalled and never-downloaded plugins are described too; plugins present locally are read from disk and never trigger a request. Failing to reach GitHub falls back to the local information instead of raising. Without an argument, `--show` details every local plugin rather than the enabled ones only.

Searching and listing never install anything — installing remains `odev plugin --enable <organization>/<repository>`.

Supporting changes:

- **`parse_plugin_manifest()`** (`odev/common/odev.py`) — reads a manifest with `ast`, taking only the module docstring and top-level literal assignments. Manifests are otherwise loaded through `exec_module`, which is fine for a repository the user explicitly installed but unacceptable for arbitrary search results. A top-level `__version__` string is what identifies a repository as an odev plugin. `plugin_module_name()` is extracted at the same time, replacing the same expression repeated in four places.
- **`GithubConnector`** (`odev/common/connectors/git.py`) — `GitConnector` requires an `organization/repository` at construction, so there was nowhere for a search to live. The API connection concern is extracted into a new base class (a pure move of the token, connection and authentication members; `GitConnector` inherits from it and keeps its public API), which gains `search_repositories()`, `get_repository()` and `get_repository_file()`. All three return `None` rather than raising when a result is missing or unreadable.

Also fixes `--show <organization>/<repository>`, which **always** reported a plugin as disabled: the name was stripped of its organization before being compared to the enabled plugins, which are stored fully qualified. A repository name on its own is now accepted too, as long as it is not ambiguous.

## Testing

- Full test suite: **165 passed** (143 → 157 → 165; 18 new tests covering the manifest parser, the connector and the three command modes), py3.14, local PostgreSQL.
- `pre-commit run --all-files` clean; `basedpyright` reports no new error against the baseline.
- Exercised against the real GitHub API from a live checkout: `--search`, `--search ai`, `--search` with no result, `--list`, and `--show` on an enabled plugin (no request), the template repository (template warning, no install hint), an archived plugin (archived warning), a non-existent repository, the shadowed `avanserv` fork, a bare unknown name, and with no argument.
- A manifest containing `os.system(...)` and `raise SystemExit(1)` at module level is parsed with no side effect (covered by a unit test).

## Compliance

- [x] I have read the [contribution guide](../docs/CONTRIBUTING.md)
- [x] I made sure the documentation is up-to-date both in doctrings and the `docs` directory
- [x] I have added or modified unit tests where necessary
- [x] I have added new libraries to the `requirements.txt` file, if any
- [x] I have incremented the version number according the [versioning guide](../../docs/contributing/versioning.md)
- [x] The PR contains **my changes only** and **no other external commit**

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01H9M1zJCzxpg35ijsDcPEPA
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.

2 participants