diff --git a/.serena/memories/repl.md b/.serena/memories/repl.md index 8729d804..84f374b1 100644 --- a/.serena/memories/repl.md +++ b/.serena/memories/repl.md @@ -30,14 +30,18 @@ Code runs as a function body (`return` defines the result); a single expression - Progressive disclosure: a priori only facade names, descriptions and method names; `s.info("")` / `s.info(".")` give signature + docstring together, never a signature alone. `info(*items)` documents several items at once; unknown items are reported inline. +- Disclosure tiers: tier 0 (tool description) = facades, descriptions, method names with navigable return types + (`find_symbol -> LspSymbolCollection`); tier 1 (`s.info("")`) = all common methods in full, + `niche` methods (`@facade_method(niche=True)`, rarely needed + long docs) only as summary + pointer, result + types by name only; tier 2 = types on request. Types are never pushed (`provide_info_with_facade` exists but + is set nowhere); the tool description tells the model to request type docs only when processing results in code. - Result types: a type returned by a single method is documented under `:return:`. Types that are shared, contained (`LanguageServerSymbol`) or navigated are declared per facade as `ReferencedType`s (constructor - arg `types=`), with `provide_info_with_facade` (full description in `s.info("")`, else listed by name) - and an optional `members` whitelist (curation for foreign/large classes; listed methods are shown even if - undocumented, convention-derived ones only if documented). Types are documented via `s.info(".")` - or bare `s.info("")`; method docs point to their referenced return type. Result classes declare - attribute annotations at class level (attributes set only in `__init__` are not discoverable). - Annotations are rendered without module paths, so signature names equal lookup names. + arg `types=`), with an optional `members` whitelist (curation for foreign/large classes; listed methods are + shown even if undocumented, convention-derived ones only if documented). Types are documented via + `s.info(".")` or bare `s.info("")`; method docs point to their referenced return type. + Result classes declare attribute annotations at class level (attributes set only in `__init__` are not + discoverable). Annotations are rendered without module paths, so signature names equal lookup names. - APIs must not import `serena.tools` at module level except for tool classes in decorators; tools import APIs locally in `_api()` (API modules refer to tool classes). diff --git a/src/serena/repl/api/edit_api.py b/src/serena/repl/api/edit_api.py index c7222eb9..adac2218 100644 --- a/src/serena/repl/api/edit_api.py +++ b/src/serena/repl/api/edit_api.py @@ -75,7 +75,7 @@ class EditApi(FacadeApi): name="edit", description="modifying content within existing files (independent of the language backend)", types=[ - ReferencedType(ReplacementPreview, provide_info_with_facade=True), + ReferencedType(ReplacementPreview), ReferencedType( ReplacementOccurrence, members=["occurrence_id", "relative_path", "start_line", "end_line", "matched_text", "replacement", "is_ambiguous"], diff --git a/src/serena/repl/api/fs_api.py b/src/serena/repl/api/fs_api.py index 5461d2ac..19b681f4 100644 --- a/src/serena/repl/api/fs_api.py +++ b/src/serena/repl/api/fs_api.py @@ -176,7 +176,7 @@ class FsApi(FacadeApi): types=[ ReferencedType(FileContent), ReferencedType(DirectoryListing), - ReferencedType(PatternMatches, provide_info_with_facade=True), + ReferencedType(PatternMatches), ReferencedType( MatchedConsecutiveLines, members=["source_file_path", "matched_lines", "start_line", "end_line", "to_display_string"] ), diff --git a/src/serena/repl/api/jb_api.py b/src/serena/repl/api/jb_api.py index 66854b61..84e8cb35 100644 --- a/src/serena/repl/api/jb_api.py +++ b/src/serena/repl/api/jb_api.py @@ -186,7 +186,7 @@ class JetBrainsApi(FacadeApi): name="jb", description="operations on the codebase backed by the JetBrains IDE's code intelligence", types=[ - ReferencedType(JetBrainsSymbolCollection, provide_info_with_facade=True), + ReferencedType(JetBrainsSymbolCollection), ReferencedType(JetBrainsSymbolsOverview), ReferencedType(JsonObject), ], diff --git a/src/serena/repl/api/lsp_api.py b/src/serena/repl/api/lsp_api.py index 449fc603..fbecf02e 100644 --- a/src/serena/repl/api/lsp_api.py +++ b/src/serena/repl/api/lsp_api.py @@ -313,7 +313,7 @@ class LspApi(FacadeApi): name="lsp", description="symbol-level operations on the codebase backed by language servers", types=[ - ReferencedType(LspSymbolCollection, provide_info_with_facade=True), + ReferencedType(LspSymbolCollection), ReferencedType( LanguageServerSymbol, members=[ diff --git a/src/serena/repl/facade.py b/src/serena/repl/facade.py index 6e7d1857..caec69fb 100644 --- a/src/serena/repl/facade.py +++ b/src/serena/repl/facade.py @@ -143,6 +143,11 @@ class FacadeMethodInfo: """whether the method is in beta (not yet fully stable)""" can_edit: bool = False """whether the method can modify the codebase (relevant for read-only contexts)""" + niche: bool = False + """ + whether the method is rarely needed, such that the facade's description only summarises it (first line of its + documentation and a pointer to its full documentation) in order to keep the description compact + """ corresponding_tool: "type[Tool] | None" = None """the classic tool offering the same functionality, if any""" @@ -157,7 +162,12 @@ _FACADE_METHOD_INFO_ATTR = "__facade_method_info__" def facade_method( - *, optional: bool = False, beta: bool = False, can_edit: bool = False, corresponding_tool: "type[Tool] | None" = None + *, + optional: bool = False, + beta: bool = False, + can_edit: bool = False, + niche: bool = False, + corresponding_tool: "type[Tool] | None" = None, ) -> Callable[[TCallable], TCallable]: """ Marks a method of a `FacadeApi` as exposed through the facade, attaching the given metadata. @@ -166,13 +176,19 @@ def facade_method( :param optional: whether the method is disabled by default and must be enabled explicitly :param beta: whether the method is in beta :param can_edit: whether the method can modify the codebase + :param niche: whether the method is rarely needed (its documentation is then only summarised in the facade's description) :param corresponding_tool: the classic tool offering the same functionality, if any :return: the decorator """ def decorator(method: TCallable) -> TCallable: info = FacadeMethodInfo( - name=method.__name__, optional=optional, beta=beta, can_edit=can_edit, corresponding_tool=corresponding_tool + name=method.__name__, + optional=optional, + beta=beta, + can_edit=can_edit, + niche=niche, + corresponding_tool=corresponding_tool, ) setattr(method, _FACADE_METHOD_INFO_ATTR, info) return method @@ -282,23 +298,40 @@ class FacadeMethod: def __call__(self, *args: Any, **kwargs: Any) -> Any: return self._implementation(*args, **kwargs) - def describe(self, include_return_type_pointer: bool = True) -> str: + def get_referenced_return_types(self) -> list[ReferencedType]: """ - :param include_return_type_pointer: whether to append a pointer to the documentation of the return type, + :return: the types referenced by the facade which appear in the method's return type annotation + """ + return_annotation = format_annotation(inspect.signature(self._implementation).return_annotation) + return [t for t in self.parent.get_types() if re.search(rf"\b{re.escape(t.name)}\b", return_annotation)] + + def get_summary(self) -> str: + """ + :return: the first line of the method's documentation + """ + doc = inspect.getdoc(self._implementation) + return doc.splitlines()[0] if doc else "(no documentation)" + + def describe(self) -> str: + """ + :return: the method's signature and documentation, with a pointer to the documentation of its return type if it is a type referenced by the facade - :return: the method's signature and documentation """ signature = format_signature(self._implementation) doc = inspect.getdoc(self._implementation) or "(no documentation)" text = f"{self.qualified_name}{signature}\n{doc}\n" - if include_return_type_pointer: - return_annotation = format_annotation(inspect.signature(self._implementation).return_annotation) - referenced = [t for t in self.parent.get_types() if re.search(rf"\b{re.escape(t.name)}\b", return_annotation)] - if referenced: - pointers = ", ".join(f'`s.info("{self.facade_name}.{t.name}")`' for t in referenced) - text += f"Return type: see {pointers}\n" + referenced = self.get_referenced_return_types() + if referenced: + pointers = ", ".join(f'`s.info("{t.name}")`' for t in referenced) + text += f"Return type documentation: {pointers}\n" return text + def describe_summary(self) -> str: + """ + :return: the method's name and the first line of its documentation, with a pointer to its full documentation + """ + return f'{self.qualified_name}: {self.get_summary()} [full documentation: `s.info("{self.qualified_name}")`]\n' + class ApiScope: """ @@ -492,18 +525,24 @@ class Facade: as well as its referenced types (in full if so declared, otherwise by name) """ parts = [f"Facade '{self._name}': {self._description}", ""] - for method in self._methods.values(): - if method.enabled: + enabled_methods = self.get_enabled_methods() + for method in enabled_methods: + if not method.info.niche: parts.append(method.describe()) - described_types = [t for t in self._types.values() if t.provide_info_with_facade] - listed_types = [t for t in self._types.values() if not t.provide_info_with_facade] - for referenced_type in described_types: - parts.append(referenced_type.describe()) - if listed_types: + niche_methods = [m for m in enabled_methods if m.info.niche] + if niche_methods: + parts.append("Rarely needed methods (documented on request):\n" + "".join(m.describe_summary() for m in niche_methods)) + for referenced_type in self._types.values(): + if referenced_type.provide_info_with_facade: + parts.append(referenced_type.describe()) + result_type_names = sorted( + {t.name for m in enabled_methods for t in m.get_referenced_return_types() if not t.provide_info_with_facade} + ) + if result_type_names: parts.append( - "Further types: " - + ", ".join(t.name for t in listed_types) - + f' (request documentation via `s.info("{self._name}.")`)' + "Result types: " + + ", ".join(result_type_names) + + ' (request documentation via `s.info("")` only if you intend to process results in code)' ) return "\n".join(parts) diff --git a/src/serena/repl/repl.py b/src/serena/repl/repl.py index 93f00834..883ab4c7 100644 --- a/src/serena/repl/repl.py +++ b/src/serena/repl/repl.py @@ -54,9 +54,15 @@ class SerenaReplEntrypoint: def overview(self) -> str: """ :return: the list of available facades, each with a one-line description and the names of its methods + (with the result type of methods returning objects that can be processed in code) """ + + def method_entry(method: FacadeMethod) -> str: + return_types = method.get_referenced_return_types() + return method.name + (f" -> {'|'.join(t.name for t in return_types)}" if return_types else "") + return "\n".join( - f"s.{facade.name}: {facade.description}\n methods: {', '.join(facade.enabled_method_names)}" + f"s.{facade.name}: {facade.description}\n methods: {', '.join(method_entry(m) for m in facade.get_enabled_methods())}" for facade in self._facades.values() ) diff --git a/src/serena/tools/repl_tools.py b/src/serena/tools/repl_tools.py index 611241e4..c8e9c2e1 100644 --- a/src/serena/tools/repl_tools.py +++ b/src/serena/tools/repl_tools.py @@ -18,9 +18,16 @@ class SerenaReplTool(Tool, ToolMarkerOptional, ToolMarkerBeta): def apply(self, code: str) -> str: """ Executes the given Python code, which has access to Serena's functionality through the object `s`. - The functionality is organised in facades, which are attributes of `s` (e.g. `s.myfacade`). - Use `s.info()` to list the facades, `s.info("")` to see a facade's methods and - `s.info(".")` for the documentation of a single method. + The functionality is organised in facades, which are attributes of `s` (e.g. `s.myfacade`); the available + facades and their methods are listed below. + + Documentation: Use `s.info("")` when you will use a facade's functionality (it documents all common + operations at once) and `s.info(".")` for a single or a rarely needed operation. Several items + can be requested in one call, e.g. `s.info("lsp", "edit.replace_content")`. + `s.info("")` documents the facade's operations only, not their result types. Result types are given + in the method listing below (`method -> Type`); request their documentation via `s.info("")`, which + includes the types they contain, ONLY if you intend to process results in code (filter, aggregate, chain + calls). If you simply want the result, return it directly: returned objects are rendered for you. The code is executed as the body of a function, so use `return` to define the result; a single expression is evaluated and its value returned directly. diff --git a/test/serena/test_repl_tool.py b/test/serena/test_repl_tool.py index da13d65d..9afe043e 100644 --- a/test/serena/test_repl_tool.py +++ b/test/serena/test_repl_tool.py @@ -53,15 +53,18 @@ class TestReplExecution: assert "name_path_pattern" in method_info def test_type_discovery(self, repl: SerenaRepl) -> None: + # the overview names the result types of methods returning objects that can be processed in code + assert "find_symbol -> LspSymbolCollection" in repl.execute("s.info()") + # signatures render type names without module paths, and point to the documentation of referenced return types method_info = repl.execute('s.info("lsp.find_symbol")') assert "-> LspSymbolCollection" in method_info and "lsp_api." not in method_info - assert 's.info("lsp.LspSymbolCollection")' in method_info + assert 's.info("LspSymbolCollection")' in method_info - # the facade description includes types declared as important in full and lists the others by name + # the facade description documents the operations only and lists the result types by name facade_info = repl.execute('s.info("lsp")') - assert "type LspSymbolCollection" in facade_info and "symbols: list[LanguageServerSymbol]" in facade_info - assert "type LanguageServerSymbol" not in facade_info and "LanguageServerSymbol" in facade_info + assert "type LspSymbolCollection" not in facade_info + assert "Result types: " in facade_info and "LspSymbolCollection" in facade_info # types can be requested via the facade or by bare name, and their curated members are documented type_info = repl.execute('s.info("lsp.LanguageServerSymbol")') @@ -96,6 +99,14 @@ class TestFacade: def extra(self) -> str: return "extra" + @facade_method(niche=True) + def rarely(self, x: int) -> str: + """Rarely needed operation. + + :param x: some parameter + """ + return str(x) + def undecorated(self) -> str: """Public within Serena, but not exposed, since it is not decorated.""" return "internal" @@ -142,13 +153,13 @@ class TestFacade: ApiInclusionDefinition(included_apis=["dummy.add"]), ) facade = Facade.from_api(self.DummyApi(MagicMock()), scope) - assert set(facade.enabled_method_names) == {"add", "secret"} + assert set(facade.enabled_method_names) == {"add", "secret", "rarely"} def test_api_scope_read_only_excludes_editing_methods(self) -> None: scope = self._scope(included_apis=["dummy.secret"]) scope.exclude_editing() facade = Facade.from_api(self.DummyApi(MagicMock()), scope) - assert facade.enabled_method_names == ["add"] + assert set(facade.enabled_method_names) == {"add", "rarely"} def test_enabled_methods_delegate_to_implementation(self) -> None: facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope()) @@ -176,6 +187,13 @@ class TestFacade: assert "undecorated" not in facade.describe() assert "undecorated" not in facade.enabled_method_names + def test_niche_methods_are_summarised_in_facade_description(self) -> None: + facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope()) + description = facade.describe() + assert "dummy.rarely: Rarely needed operation." in description + assert ":param x:" not in description # only the summary, no signature or full documentation + assert ":param x:" in facade.describe_member("rarely") # full documentation on request + def test_optional_methods_are_disabled_by_default(self) -> None: facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope()) assert "extra" not in facade.enabled_method_names