Skip to content

Scope Python RPC marketplace responses to the requested distribution#7512

Merged
jkschneider merged 4 commits intomainfrom
python-rpc-pip-attribution
Apr 29, 2026
Merged

Scope Python RPC marketplace responses to the requested distribution#7512
jkschneider merged 4 commits intomainfrom
python-rpc-pip-attribution

Conversation

@jkschneider
Copy link
Copy Markdown
Member

Summary

discover_recipes() eagerly walked every openrewrite.recipes entry point on _get_marketplace() init, and handle_get_marketplace() returned the full singleton — so installing any pip package had its RecipeBundle tagged with every recipe in the process, including the eight built-in org.openrewrite.python.* recipes activated by the openrewrite distribution itself. A visualization-only package like moderne-visualizations-misc ended up "owning" every Python recipe.

  • Track per-distribution attribution at activation time ({distribution_name -> set(recipe_name)}).
  • Have handle_install_recipes return the rows for just the requested package.
  • Let handle_get_marketplace filter by packageName when supplied; behavior is unchanged when no filter is provided.
  • PipRecipeBundleReader now constructs its marketplace from the install response directly instead of issuing a follow-up GetMarketplace call that would still see the full singleton.
  • InstallRecipesResponse carries the new recipes field backwards-compatibly: older Python servers that don't include it deserialize to null, accessed via recipesOrEmpty().

Pairs with the moderne-saas fix that re-enables the pip flow into the recipe marketplace.

Test plan

  • pytest tests/ — 1,125 passed, 56 skipped.
  • ./gradlew :rewrite-python:test :rewrite-core:test --tests RewriteRpcTest — BUILD SUCCESSFUL.
  • New regression tests in test_marketplace.py::TestPerPackageAttribution:
    • Two recipe-bearing packages each get only their own recipes back.
    • Visualization-only package (no entry points) gets [], not the eight built-ins.
    • GetMarketplace filter by packageName returns only that package's recipes; no-filter still returns everything.
    • Hyphen/underscore name variants resolve to the same package.

`discover_recipes()` eagerly walked every `openrewrite.recipes` entry
point on `_get_marketplace()` init and `handle_get_marketplace()`
returned the full singleton, so installing any pip package had its
`RecipeBundle` tagged with every recipe in the process - including the
eight built-in `org.openrewrite.python.*` recipes activated by the
`openrewrite` distribution itself. A visualization-only package like
`moderne-visualizations-misc` ended up "owning" every Python recipe.

Track per-distribution attribution at activation time
(`{distribution_name -> set(recipe_name)}`), have
`handle_install_recipes` return only the rows for the requested
package, and let `handle_get_marketplace` filter by `packageName` when
supplied. `PipRecipeBundleReader` now uses the install response
directly instead of issuing a follow-up `GetMarketplace` call that
would still see the full singleton.

`InstallRecipesResponse` carries the new `recipes` field
backwards-compatibly: older Python servers that don't include it
deserialize to `null`, accessed through `recipesOrEmpty()`.
The number of built-in `org.openrewrite.python.*` recipes will change
over time. Drop the hard-coded "eight" from the comments and test
docstrings so they don't decay.
`Optional[Dict[str, Set[str]]]` told the reader nothing about what the
keys or values mean, and pushed PEP 503 normalization out to every
call site. Replace it with a small `RecipeAttribution` dataclass that
encapsulates the storage, normalizes distribution names on both
record and lookup, and exposes named `record(...)` / `recipes_for(...)`
methods. Export it from the package since it's now part of the
`discover_recipes` signature.
…ntic

- `recipesInstalled` is back to the diff semantic (recipes added by THIS
  call, zero on idempotent reinstalls), matching the field name's promise
  and the prior wire contract. The new `recipes` field carries the
  cumulative per-distribution row list separately. Audit of other
  language servers (Java in-process, C# LanguageServer, JS, Go) confirms
  Python is the sole InstallRecipes producer and no consumer reads
  `recipesInstalled` today, so the diff semantic is the safe default.
- Drop `@dataclass` on `RecipeAttribution`. The auto-generated
  `__init__(_by_package=...)` exposed the internal map through the
  constructor signature and we don't use the auto `__eq__`/`__repr__`.
  Plain class with `__init__(self) -> None` keeps the storage truly
  private.
- Replace bare `Dict[str, Set[str]]` with NewType-typed
  `Dict[NormalizedDistName, Set[RecipeName]]`. `NormalizedDistName` can
  only be built via `_normalize_package_name`; `RecipeName` propagates
  through `recipe_name_set`, the attribution API, and
  `_collect_marketplace_rows`'s filter parameter. Zero runtime cost,
  type-checker discipline at every site.
@jkschneider jkschneider merged commit aaf2de6 into main Apr 29, 2026
1 check passed
@jkschneider jkschneider deleted the python-rpc-pip-attribution branch April 29, 2026 02:43
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant