Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions rewrite-python/rewrite/src/rewrite/recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from typing import (
Any,
Callable,
Dict,
List,
Optional,
TYPE_CHECKING,
Expand Down Expand Up @@ -121,8 +122,21 @@ def from_recipe(cls, recipe: Recipe) -> RecipeDescriptor:
value = getattr(recipe, f.name)
options.append((f.name, value, descriptor))

# Extract data table descriptors
data_tables = [dt.descriptor() for dt in recipe.data_tables]
recipe_list = [cls.from_recipe(r) for r in recipe.recipe_list()]

# A recipe reports the union of the data tables it owns and every table its
# children produce, so consumers that read only the top-level descriptor --
# the marketplace listing and everything derived from it -- can resolve a
# composite's tables. Mirrors Java's Recipe#aggregateDataTableDescriptors.
# Sub-descriptors are built the same way, so the union is recursive; the
# recipe's own tables come first and the first descriptor for a given name
# wins.
data_tables: Dict[str, dict] = {}
for data_table in recipe.data_tables:
data_tables.setdefault(data_table.name, data_table.descriptor())
for sub_recipe in recipe_list:
for descriptor in sub_recipe.data_tables:
data_tables.setdefault(descriptor["name"], descriptor)

return cls(
name=recipe.name,
Expand All @@ -131,8 +145,8 @@ def from_recipe(cls, recipe: Recipe) -> RecipeDescriptor:
tags=recipe.tags,
estimated_effort_per_occurrence=recipe.estimated_effort_per_occurrence,
options=options,
data_tables=data_tables,
recipe_list=[cls.from_recipe(r) for r in recipe.recipe_list()],
data_tables=list(data_tables.values()),
recipe_list=recipe_list,
)


Expand Down
21 changes: 19 additions & 2 deletions rewrite-python/rewrite/src/rewrite/rpc/rpc_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@

import logging
from dataclasses import dataclass
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional

from rewrite.data_table import DataTable
from rewrite.recipe import Recipe
from rewrite.tree import Tree
from rewrite.visitor import TreeVisitor
Expand Down Expand Up @@ -83,20 +84,36 @@ def recipe_list(self):
``RpcRecipe`` on the Java and JavaScript sides) is future work.
"""

def __init__(self, name: str, **options: Any):
def __init__(self, name: str, data_tables: Optional[List[DataTable]] = None,
**options: Any):
self._name = name
# ``java_recipe_name`` / ``delegates_to_options`` are the (internal)
# attribute names the server's delegatesTo production reads; see
# ``handle_prepare_recipe`` in ``rpc/server.py``. The over-the-wire
# delegatesTo payload is the ecosystem-neutral ``{recipeName, options}``.
self.java_recipe_name = name
self.delegates_to_options: Dict[str, Any] = dict(options)
self._data_tables: List[DataTable] = list(data_tables or [])
self._prepared: Optional["PreparedJavaRecipe"] = None

@property
def name(self) -> str:
return self._name

@property
def data_tables(self) -> List[DataTable]:
"""Data tables the delegate produces.

The reference carries only an id + options, so the delegate lives on another
peer and its tables cannot be introspected from here -- a descriptor is built
during marketplace registration, when no peer is necessarily connected. The
composite author declares them instead, matching the delegate's table names
and columns, so the composite's descriptor advertises everything its run will
produce. Rows are still written by the peer that owns the recipe; declaring
the table here only makes it resolvable by descriptor consumers.
"""
return self._data_tables

@property
def display_name(self) -> str:
return self._name
Expand Down
178 changes: 178 additions & 0 deletions rewrite-python/rewrite/tests/test_composite_data_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""Data tables produced by a composite's children must appear on the composite's descriptor.

Mirrors Java's ``Recipe#aggregateDataTableDescriptors``: a composite reports the
union of its own tables and every table its ``recipe_list()`` produces, so the
marketplace listing (and everything downstream of it) can resolve them.
"""

from dataclasses import dataclass, field
from typing import Any, List

from rewrite import ExecutionContext
from rewrite.data_table import DataTable, column
from rewrite.recipe import Recipe, ScanningRecipe
from rewrite.rpc import RpcRecipe


@dataclass
class UsageRow:
source_path: str = field(metadata=column("source_path", "Path of the file."))
library: str = field(metadata=column("library", "The library used."))


LEAF_TABLE_NAME = "org.example.table.LibraryUsage"
NESTED_TABLE_NAME = "org.example.table.NestedUsage"
DELEGATE_TABLE_NAME = "org.example.table.ComputeResource"


@dataclass
class FindLibraryUsage(ScanningRecipe[List[Any]]):
"""Leaf recipe that owns a data table."""

_table = DataTable(LEAF_TABLE_NAME, "Library usage", "One row per file.", UsageRow)

@property
def name(self) -> str:
return "org.example.FindLibraryUsage"

@property
def display_name(self) -> str:
return "Find library usage"

@property
def description(self) -> str:
return "Leaf recipe that owns a data table."

@property
def data_tables(self) -> List[DataTable]:
return [self._table]

def initial_value(self, ctx: ExecutionContext) -> List[Any]:
return []


@dataclass
class FindLibraryUsageByResource(Recipe):
"""Composite that owns no table of its own."""

@property
def name(self) -> str:
return "org.example.FindLibraryUsageByResource"

@property
def display_name(self) -> str:
return "Find library usage by resource"

@property
def description(self) -> str:
return "Composite whose data tables come only from its children."

def recipe_list(self) -> List[Recipe]:
return [FindLibraryUsage()]


@dataclass
class JsonPreset(Recipe):
"""Preset one level above the composite, to prove aggregation is recursive."""

@property
def name(self) -> str:
return "org.example.JsonPreset"

@property
def display_name(self) -> str:
return "Json preset"

@property
def description(self) -> str:
return "Turnkey preset over the composite."

def recipe_list(self) -> List[Recipe]:
return [FindLibraryUsageByResource()]


def _names(descriptor) -> List[str]:
return [dt["name"] for dt in descriptor.data_tables]


def test_leaf_reports_its_own_table():
assert _names(FindLibraryUsage().descriptor()) == [LEAF_TABLE_NAME]


def test_composite_aggregates_child_data_tables():
descriptor = FindLibraryUsageByResource().descriptor()
assert _names(descriptor.recipe_list[0]) == [LEAF_TABLE_NAME]
assert _names(descriptor) == [LEAF_TABLE_NAME]


def test_aggregation_is_recursive():
assert _names(JsonPreset().descriptor()) == [LEAF_TABLE_NAME]


def test_aggregated_table_keeps_its_columns():
(table,) = JsonPreset().descriptor().data_tables
assert [c["name"] for c in table["columns"]] == ["source_path", "library"]


def test_own_table_precedes_child_tables_and_duplicates_collapse():
@dataclass
class Parent(Recipe):
_table = DataTable(NESTED_TABLE_NAME, "Nested", "Parent's own table.", UsageRow)

@property
def name(self) -> str:
return "org.example.Parent"

@property
def display_name(self) -> str:
return "Parent"

@property
def description(self) -> str:
return "Owns a table and also wraps children."

@property
def data_tables(self) -> List[DataTable]:
return [self._table]

def recipe_list(self) -> List[Recipe]:
# Same leaf twice: the union must not duplicate it.
return [FindLibraryUsage(), FindLibraryUsage()]

assert _names(Parent().descriptor()) == [NESTED_TABLE_NAME, LEAF_TABLE_NAME]


def test_rpc_recipe_reports_declared_delegate_tables():
"""An RpcRecipe references a recipe on another peer by name only, so the
delegate's tables cannot be introspected locally; the composite author
declares them and they aggregate like any other child's."""
delegate_table = DataTable(
DELEGATE_TABLE_NAME, "Compute resources", "Resources from the delegate.", UsageRow
)

@dataclass
class WithDelegate(Recipe):
@property
def name(self) -> str:
return "org.example.WithDelegate"

@property
def display_name(self) -> str:
return "With delegate"

@property
def description(self) -> str:
return "Composite delegating to a recipe on another peer."

def recipe_list(self) -> List[Recipe]:
return [
FindLibraryUsage(),
RpcRecipe("org.example.java.FindComputeResources",
data_tables=[delegate_table]),
]

assert _names(WithDelegate().descriptor()) == [LEAF_TABLE_NAME, DELEGATE_TABLE_NAME]


def test_rpc_recipe_without_declared_tables_reports_none():
assert RpcRecipe("org.example.java.Whatever").data_tables == []