From 6cb3d37e094fea6b89c98d38710ee71f20fb12f6 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 8 Sep 2026 02:32:39 +0200 Subject: [PATCH] Add referenced types to facades, making result types discoverable in the REPL Facades declare the types their methods return or contain as ReferencedTypes (with an optional whitelist of members for curation, e.g. for LanguageServerSymbol, and a flag for inclusion in the facade description). Type documentation is generated from class docstrings, attribute annotations, properties and method signatures/docstrings, and is available via s.info(".") or by bare type name; method documentation points to the documentation of a referenced return type. Annotations are rendered without module paths, such that the names in signatures are the names by which types can be looked up. s.info accepts several items at once, reporting unknown items inline. All APIs declare their result types; result classes declare their attributes at class level. --- .serena/memories/repl.md | 9 ++ src/serena/repl/api/edit_api.py | 9 +- src/serena/repl/api/fs_api.py | 23 +++- src/serena/repl/api/jb_api.py | 12 ++- src/serena/repl/api/lsp_api.py | 33 +++++- src/serena/repl/api/mem_api.py | 3 +- src/serena/repl/api/shell_api.py | 4 +- src/serena/repl/facade.py | 177 ++++++++++++++++++++++++++++--- src/serena/repl/repl.py | 33 ++++-- src/serena/repl/representable.py | 3 + test/serena/test_repl_tool.py | 27 ++++- 11 files changed, 299 insertions(+), 34 deletions(-) diff --git a/.serena/memories/repl.md b/.serena/memories/repl.md index 8172b170..8729d804 100644 --- a/.serena/memories/repl.md +++ b/.serena/memories/repl.md @@ -29,6 +29,15 @@ Code runs as a function body (`return` defines the result); a single expression - Results expose data to code (`.symbols`, `.occurrences`, `.lines`, ...) and render like the classic tool output. - 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. +- 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. - 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 fa1fa186..c7222eb9 100644 --- a/src/serena/repl/api/edit_api.py +++ b/src/serena/repl/api/edit_api.py @@ -18,7 +18,7 @@ from serena.tools import ( ) from serena.util.text_utils import ContentReplacer, MultiFileReplacement, ReplacementOccurrence, ReplacementRejectedError -from ..facade import SUCCESS_RESULT, FacadeApi, facade_method +from ..facade import SUCCESS_RESULT, FacadeApi, ReferencedType, facade_method from ..representable import Renderer, RepresentableViaRenderer if TYPE_CHECKING: @@ -74,6 +74,13 @@ class EditApi(FacadeApi): agent, name="edit", description="modifying content within existing files (independent of the language backend)", + types=[ + ReferencedType(ReplacementPreview, provide_info_with_facade=True), + ReferencedType( + ReplacementOccurrence, + members=["occurrence_id", "relative_path", "start_line", "end_line", "matched_text", "replacement", "is_ambiguous"], + ), + ], ) # file-level operations diff --git a/src/serena/repl/api/fs_api.py b/src/serena/repl/api/fs_api.py index 4272a4ba..5461d2ac 100644 --- a/src/serena/repl/api/fs_api.py +++ b/src/serena/repl/api/fs_api.py @@ -14,7 +14,7 @@ from serena.util.file_system import scan_directory from serena.util.text_utils import MatchedConsecutiveLines from solidlsp.ls_utils import TextUtils -from ..facade import FacadeApi, facade_method +from ..facade import FacadeApi, ReferencedType, facade_method from ..representable import Renderer, RepresentableViaRenderer if TYPE_CHECKING: @@ -34,6 +34,8 @@ class FileContent(RepresentableViaRenderer): super().__init__(renderer) self.lines = lines + lines: list[str] + @property def text(self) -> str: return "\n".join(self.lines) @@ -59,6 +61,9 @@ class DirectoryListing(RepresentableViaRenderer): self.dirs = dirs self.files = files + dirs: list[str] + files: list[str] + class DirectoryListingRenderer(Renderer[DirectoryListing]): def render(self, obj: DirectoryListing) -> str: @@ -79,6 +84,8 @@ class PatternMatches(RepresentableViaRenderer): super().__init__(renderer) self.matches = matches + matches: list[MatchedConsecutiveLines] + def __len__(self) -> int: return len(self.matches) @@ -162,7 +169,19 @@ class PatternMatchesRenderer(Renderer[PatternMatches]): class FsApi(FacadeApi): def __init__(self, agent: "SerenaAgent") -> None: - super().__init__(agent, name="fs", description="the project's files as units (as opposed to their content, see `edit`)") + super().__init__( + agent, + name="fs", + description="the project's files as units (as opposed to their content, see `edit`)", + types=[ + ReferencedType(FileContent), + ReferencedType(DirectoryListing), + ReferencedType(PatternMatches, provide_info_with_facade=True), + ReferencedType( + MatchedConsecutiveLines, members=["source_file_path", "matched_lines", "start_line", "end_line", "to_display_string"] + ), + ], + ) @facade_method(corresponding_tool=ReadFileTool) def read_file(self, relative_path: str, start_line: int = 0, end_line: int | None = None, max_answer_chars: int = -1) -> FileContent: diff --git a/src/serena/repl/api/jb_api.py b/src/serena/repl/api/jb_api.py index e8ffdb62..66854b61 100644 --- a/src/serena/repl/api/jb_api.py +++ b/src/serena/repl/api/jb_api.py @@ -30,7 +30,7 @@ from serena.tools import ( ) from serena.util.text_utils import find_text_coordinates -from ..facade import FacadeApi, facade_method +from ..facade import FacadeApi, ReferencedType, facade_method from ..representable import JsonObject, JsonObjectRenderer, Renderer, RepresentableViaRenderer if TYPE_CHECKING: @@ -44,6 +44,8 @@ class JetBrainsSymbolCollection(RepresentableViaRenderer): `children`, `body`, `quick_info`, `documentation` and (for references) `context`. """ + symbols: list[SymbolDTO] + def __init__(self, symbols: list[SymbolDTO], renderer: "JetBrainsSymbolCollectionRenderer"): """ :param symbols: the symbols @@ -123,6 +125,9 @@ class JetBrainsSymbolsOverview(RepresentableViaRenderer): self.symbols = symbols self.documentation = documentation + symbols: list[SymbolDTO] + documentation: str | None + class JetBrainsSymbolsOverviewRenderer(Renderer[JetBrainsSymbolsOverview]): """ @@ -180,6 +185,11 @@ class JetBrainsApi(FacadeApi): agent, name="jb", description="operations on the codebase backed by the JetBrains IDE's code intelligence", + types=[ + ReferencedType(JetBrainsSymbolCollection, provide_info_with_facade=True), + ReferencedType(JetBrainsSymbolsOverview), + ReferencedType(JsonObject), + ], ) @contextmanager diff --git a/src/serena/repl/api/lsp_api.py b/src/serena/repl/api/lsp_api.py index 28015197..449fc603 100644 --- a/src/serena/repl/api/lsp_api.py +++ b/src/serena/repl/api/lsp_api.py @@ -33,7 +33,7 @@ from serena.tools import ( from serena.util.text_utils import TextOutputUtils, find_text_coordinates from solidlsp.lsp_protocol_handler.lsp_types import SymbolKind -from ..facade import SUCCESS_RESULT, FacadeApi, facade_method +from ..facade import SUCCESS_RESULT, FacadeApi, ReferencedType, facade_method from ..representable import Renderer, RepresentableViaRenderer if TYPE_CHECKING: @@ -47,6 +47,8 @@ class LspSymbolCollection(RepresentableViaRenderer): `body`, `get_body_line_numbers()`, `iter_children()`. """ + symbols: list[LanguageServerSymbol] + def __init__(self, symbols: list[LanguageServerSymbol], renderer: "LspSymbolCollectionRenderer"): """ :param symbols: the list of symbols @@ -70,6 +72,8 @@ class LspSymbol(RepresentableViaRenderer): A single symbol retrieved via the language server (see `LspSymbolCollection` for the symbol's interface). """ + symbol: LanguageServerSymbol + def __init__(self, symbol: LanguageServerSymbol, renderer: "LspSymbolRenderer"): """ :param symbol: the symbol @@ -207,6 +211,8 @@ class LspReferenceCollection(RepresentableViaRenderer): (a `LanguageServerSymbol`) and the `line` of the reference. """ + references: list[ReferenceInLanguageServerSymbol] + def __init__(self, references: list[ReferenceInLanguageServerSymbol], renderer: "LspReferenceCollectionRenderer"): """ :param references: the references @@ -273,6 +279,8 @@ class LspDiagnostics(RepresentableViaRenderer): Diagnostics grouped as `relative_path -> severity -> name_path -> diagnostics`; see `grouped.get_dict()`. """ + grouped: GroupedDiagnostics + def __init__(self, grouped: GroupedDiagnostics, renderer: "LspDiagnosticsRenderer"): """ :param grouped: the grouped diagnostics @@ -304,6 +312,29 @@ class LspApi(FacadeApi): agent, name="lsp", description="symbol-level operations on the codebase backed by language servers", + types=[ + ReferencedType(LspSymbolCollection, provide_info_with_facade=True), + ReferencedType( + LanguageServerSymbol, + members=[ + "name", + "get_name_path", + "relative_path", + "symbol_kind_name", + "line", + "column", + "body", + "get_body_line_numbers", + "iter_children", + "iter_ancestors", + "get_parent", + ], + ), + ReferencedType(LspSymbol), + ReferencedType(LspReferenceCollection), + ReferencedType(ReferenceInLanguageServerSymbol), + ReferencedType(LspDiagnostics), + ], ) def _create_symbol_retriever(self) -> LanguageServerSymbolRetriever: diff --git a/src/serena/repl/api/mem_api.py b/src/serena/repl/api/mem_api.py index d6e41f76..36bb2fdc 100644 --- a/src/serena/repl/api/mem_api.py +++ b/src/serena/repl/api/mem_api.py @@ -18,7 +18,7 @@ from serena.tools import ( WriteMemoryTool, ) -from ..facade import FacadeApi, facade_method +from ..facade import FacadeApi, ReferencedType, facade_method from ..representable import Renderer, RepresentableViaRenderer if TYPE_CHECKING: @@ -60,6 +60,7 @@ class MemoryApi(FacadeApi): agent, name="mem", description="project memories, i.e. persistent notes for future tasks", + types=[ReferencedType(MemoryList)], ) def _get_memory_manager(self) -> MemoryManager: diff --git a/src/serena/repl/api/shell_api.py b/src/serena/repl/api/shell_api.py index 56ba2679..801b8e93 100644 --- a/src/serena/repl/api/shell_api.py +++ b/src/serena/repl/api/shell_api.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING from serena.tools import ExecuteShellCommandTool from serena.util.shell import ShellCommandResult, execute_shell_command -from ..facade import FacadeApi, facade_method +from ..facade import FacadeApi, ReferencedType, facade_method from ..representable import Renderer, RepresentableViaRenderer if TYPE_CHECKING: @@ -53,7 +53,7 @@ class ShellCommandOutputRenderer(Renderer[ShellCommandOutput]): class ShellApi(FacadeApi): def __init__(self, agent: "SerenaAgent") -> None: - super().__init__(agent, name="shell", description="execution of shell commands") + super().__init__(agent, name="shell", description="execution of shell commands", types=[ReferencedType(ShellCommandOutput)]) @facade_method(can_edit=True, corresponding_tool=ExecuteShellCommandTool) def execute_shell_command( diff --git a/src/serena/repl/facade.py b/src/serena/repl/facade.py index 987b7598..6e7d1857 100644 --- a/src/serena/repl/facade.py +++ b/src/serena/repl/facade.py @@ -6,14 +6,18 @@ The facade, i.e. the object through which REPL code accesses a group of related import inspect import logging +import re +import typing from abc import ABC -from collections.abc import Callable +from collections.abc import Callable, Sequence from dataclasses import dataclass from typing import TYPE_CHECKING, Any, TypeVar from serena.config.serena_config import ApiInclusionDefinition from serena.project import Project +from .representable import RepresentableViaRenderer + if TYPE_CHECKING: from serena.agent import SerenaAgent from serena.code_editor import CodeEditor @@ -26,6 +30,105 @@ SUCCESS_RESULT = "OK" """the result returned by operations which have no result other than their success""" +def format_annotation(annotation: Any) -> str: + """ + :param annotation: a type annotation (or a signature/annotation string) + :return: the annotation rendered without module paths (e.g. `list[LanguageServerSymbol]`), such that type names + match the names by which the types can be looked up + """ + text = annotation if isinstance(annotation, str) else inspect.formatannotation(annotation) + return re.sub(r"\b(?:[A-Za-z_]\w*\.)+([A-Za-z_]\w*)", r"\1", text) + + +def format_signature(callable_: Callable[..., Any]) -> str: + """ + :param callable_: the callable + :return: the signature rendered without module paths in annotations + """ + return format_annotation(str(inspect.signature(callable_))) + + +@dataclass +class ReferencedType: + """ + A type that is referenced by a facade's methods (returned by them or contained in their results), whose interface + the LLM can inspect via `info`. + """ + + cls: type + """the type""" + provide_info_with_facade: bool = False + """whether the type's full description is included in the facade's description (rather than just its name)""" + members: Sequence[str] | None = None + """ + the LLM-facing members (attributes, properties, methods) to describe; if None, all members admitted by the naming + convention (no leading or trailing underscore) which are documented are described. Explicitly listed methods are + described even if undocumented, as the listing is the documentation decision. + """ + + _CAPABILITIES: typing.ClassVar[dict[str, str]] = {"__len__": "len()", "__iter__": "iteration", "__getitem__": "indexing"} + + @property + def name(self) -> str: + return self.cls.__name__ + + def _get_member_names(self) -> list[str]: + if self.members is not None: + return list(self.members) + names = set(typing.get_type_hints(self.cls)) + names.update(n for n in dir(self.cls) if not n.startswith("_")) + # exclude the representation mechanism, which is not meant to be used from REPL code + names.difference_update(dir(RepresentableViaRenderer)) + return sorted(n for n in names if not n.startswith("_") and not n.endswith("_")) + + @staticmethod + def _first_doc_line(obj: Any) -> str: + doc = inspect.getdoc(obj) or "" + first_line = doc.splitlines()[0] if doc else "" + return first_line.removeprefix(":return:").strip() + + def describe(self) -> str: + """ + :return: the type's documentation: its docstring, attributes/properties with their types and methods with their + signatures and documentation + """ + attributes: list[str] = [] + methods: list[str] = [] + type_hints = typing.get_type_hints(self.cls) + for member_name in self._get_member_names(): + member = inspect.getattr_static(self.cls, member_name, None) + if isinstance(member, property): + fget = member.fget + annotation = inspect.signature(fget).return_annotation if fget is not None else inspect.Signature.empty + type_str = f": {format_annotation(annotation)}" if annotation is not inspect.Signature.empty else "" + doc = self._first_doc_line(member) + attributes.append(f" {member_name}{type_str}" + (f" # {doc}" if doc else "")) + elif inspect.isfunction(member): + doc = inspect.getdoc(member) + if doc is None and self.members is None: + continue + signature = format_signature(member).replace("(self, ", "(", 1).replace("(self)", "()", 1) + methods.append(f" {member_name}{signature}" + (f"\n {doc.replace(chr(10), chr(10) + ' ')}" if doc else "")) + elif member_name in type_hints: + attributes.append(f" {member_name}: {format_annotation(type_hints[member_name])}") + else: + attributes.append(f" {member_name}") + + # assemble the description + parts = [f"type {self.name}"] + if self.cls.__doc__: # NOTE: the class' own docstring (inspect.getdoc would fall back to base class docstrings) + doc = inspect.cleandoc(self.cls.__doc__) + parts.append(f" {doc.replace(chr(10), chr(10) + ' ')}") + if attributes: + parts.append("attributes:\n" + "\n".join(attributes)) + if methods: + parts.append("methods:\n" + "\n".join(methods)) + capabilities = [text for dunder, text in self._CAPABILITIES.items() if dunder in dir(self.cls) and dunder not in dir(object)] + if capabilities: + parts.append("supports: " + ", ".join(capabilities)) + return "\n".join(parts) + "\n" + + @dataclass(kw_only=True, frozen=True) class FacadeMethodInfo: """ @@ -101,15 +204,18 @@ class FacadeApi(ABC): * Names with a leading underscore are private, as usual. """ - def __init__(self, agent: "SerenaAgent", name: str, description: str) -> None: + def __init__(self, agent: "SerenaAgent", name: str, description: str, types: Sequence[ReferencedType] = ()) -> None: """ :param agent: the agent providing access to the project and its resources :param name: the attribute name under which the facade is accessible from the REPL entrypoint :param description: a one-line description of the functionality offered by the facade + :param types: the types referenced by the facade's methods (returned or contained in results) whose interface + the LLM shall be able to inspect """ self._agent = agent self._name = name self._description = description + self._types = list(types) def get_name_(self) -> str: return self._name @@ -117,6 +223,9 @@ class FacadeApi(ABC): def get_description_(self) -> str: return self._description + def get_referenced_types_(self) -> list[ReferencedType]: + return self._types + def _get_project(self) -> Project: return self._agent.get_active_project_or_raise() @@ -173,13 +282,22 @@ class FacadeMethod: def __call__(self, *args: Any, **kwargs: Any) -> Any: return self._implementation(*args, **kwargs) - def describe(self) -> str: + def describe(self, include_return_type_pointer: bool = True) -> str: """ + :param include_return_type_pointer: whether to append a pointer to the documentation of the return type, + if it is a type referenced by the facade :return: the method's signature and documentation """ - signature = inspect.signature(self._implementation) + signature = format_signature(self._implementation) doc = inspect.getdoc(self._implementation) or "(no documentation)" - return f"{self.qualified_name}{signature}\n{doc}\n" + 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" + return text class ApiScope: @@ -282,11 +400,12 @@ class Facade: A named group of related operations which an LLM can invoke from REPL code. """ - def __init__(self, name: str, description: str) -> None: + def __init__(self, name: str, description: str, types: Sequence[ReferencedType] = ()) -> None: # NOTE: attributes are set via object.__setattr__ because __getattr__ is overridden object.__setattr__(self, "_name", name) object.__setattr__(self, "_description", description) object.__setattr__(self, "_methods", {}) + object.__setattr__(self, "_types", {t.name: t for t in types}) def _add_method(self, method: FacadeMethod) -> None: assert method.parent is self @@ -301,7 +420,7 @@ class Facade: :param api_scope: API scope definition determining which methods are enabled :return: the facade """ - facade = Facade(api.get_name_(), api.get_description_()) + facade = Facade(api.get_name_(), api.get_description_(), api.get_referenced_types_()) for name, member in inspect.getmembers(api, predicate=inspect.ismethod): method_info = get_facade_method_info(member) if method_info is None: @@ -328,6 +447,19 @@ class Facade: """ return [m for m in self._methods.values() if m.enabled] + def get_types(self) -> list[ReferencedType]: + """ + :return: the types referenced by the facade's methods + """ + return list(self._types.values()) + + def get_type(self, type_name: str) -> ReferencedType | None: + """ + :param type_name: the name of the type + :return: the referenced type, or None if the facade does not reference a type of that name + """ + return self._types.get(type_name) + def get_method(self, method_name: str) -> FacadeMethod: """ :param method_name: the name of the method @@ -357,19 +489,36 @@ class Facade: def describe(self) -> str: """ :return: a description of the facade listing all of its enabled methods with their signatures and documentation + 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: 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: + parts.append( + "Further types: " + + ", ".join(t.name for t in listed_types) + + f' (request documentation via `s.info("{self._name}.")`)' + ) return "\n".join(parts) - def describe_method(self, method_name: str) -> str: + def describe_member(self, member_name: str) -> str: """ - :param method_name: the name of one of the facade's enabled methods - :return: the method's signature and documentation + :param member_name: the name of one of the facade's enabled methods or referenced types + :return: the member's documentation """ - method = self._get_enabled_method(method_name) - if method is None: - raise ValueError(self._no_such_method_message(method_name)) - return method.describe() + method = self._get_enabled_method(member_name) + if method is not None: + return method.describe() + referenced_type = self._types.get(member_name) + if referenced_type is not None: + return referenced_type.describe() + raise ValueError( + f"Facade '{self._name}' has no method or type '{member_name}'. " + f"Available methods: {self.enabled_method_names}; types: {list(self._types)}" + ) diff --git a/src/serena/repl/repl.py b/src/serena/repl/repl.py index 79139388..93f00834 100644 --- a/src/serena/repl/repl.py +++ b/src/serena/repl/repl.py @@ -60,22 +60,35 @@ class SerenaReplEntrypoint: for facade in self._facades.values() ) - def info(self, path: str = "") -> str: + def info(self, *items: str) -> str: """ Provides documentation on the available functionality. - :param path: the empty string for an overview of all facades, a facade name (e.g. "lsp") for the - documentation of all of the facade's methods, or a dotted method path (e.g. "lsp.find_symbol") - for the documentation of a single method + :param items: the items to document; if none are given, an overview of all facades is provided. + Each item is either a facade name (e.g. "lsp") for the documentation of all of the facade's methods and types, + a dotted path (e.g. "lsp.find_symbol" or "lsp.LspSymbolCollection") for the documentation of a single method + or type, or a bare type name (e.g. "LanguageServerSymbol"), which is looked up across all facades. + Unknown items are reported without affecting the documentation of the other items. :return: the requested documentation """ - if path == "": + if not items: return self.overview() - facade_name, _, method_name = path.partition(".") - facade = self._get_facade(facade_name) - if method_name == "": - return facade.describe() - return facade.describe_method(method_name) + return "\n\n".join(self._describe_item(item) for item in items) + + def _describe_item(self, item: str) -> str: + facade_name, _, member_name = item.partition(".") + try: + if member_name: + return self._get_facade(facade_name).describe_member(member_name) + if facade_name in self._facades: + return self._facades[facade_name].describe() + # not a facade: look up the item as a type across all facades + matches = [facade for facade in self._facades.values() if facade.get_type(item) is not None] + if not matches: + raise ValueError(f"Unknown item '{item}': neither a facade nor a type. Available facades: {list(self._facades)}") + return matches[0].describe_member(item) + except ValueError as e: + return str(e) class SerenaRepl: diff --git a/src/serena/repl/representable.py b/src/serena/repl/representable.py index 1b89a40e..9867f726 100644 --- a/src/serena/repl/representable.py +++ b/src/serena/repl/representable.py @@ -92,6 +92,9 @@ class JsonObject(RepresentableViaRenderer): super().__init__(renderer) self.data = data + data: Any + """the JSON-serializable data (dict, list, etc.)""" + class JsonObjectRenderer(Renderer[JsonObject]): def render(self, obj: JsonObject) -> str: diff --git a/test/serena/test_repl_tool.py b/test/serena/test_repl_tool.py index fc04c434..da13d65d 100644 --- a/test/serena/test_repl_tool.py +++ b/test/serena/test_repl_tool.py @@ -52,6 +52,29 @@ class TestReplExecution: method_info = repl.execute('s.info("lsp.find_symbol")') assert "name_path_pattern" in method_info + def test_type_discovery(self, repl: SerenaRepl) -> None: + # 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 + + # the facade description includes types declared as important in full and lists the others 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 + + # 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")') + assert type_info == repl.execute('s.info("LanguageServerSymbol")') + assert "get_name_path() -> str" in type_info and "iter_children()" in type_info + assert "to_dict" not in type_info # not among the curated members + assert "represent" not in repl.execute('s.info("LspSymbolCollection")') # the representation mechanism is not exposed + + def test_info_documents_several_items(self, repl: SerenaRepl) -> None: + info = repl.execute('s.info("lsp.find_symbol", "nope", "lsp.LspSymbolCollection")') + assert "lsp.find_symbol(" in info and "type LspSymbolCollection" in info + assert "Unknown item 'nope'" in info # an unknown item does not prevent the documentation of the others + class TestFacade: """Tests the indirection between facades and their implementations.""" @@ -131,7 +154,7 @@ class TestFacade: facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope()) assert facade.add(1, 2) == 3 assert "dummy.add(a: int, b: int) -> int" in facade.describe() - assert "Adds two numbers." in facade.describe_method("add") + assert "Adds two numbers." in facade.describe_member("add") def test_disabled_methods_are_inaccessible_and_undocumented(self) -> None: facade = Facade.from_api(self.DummyApi(MagicMock()), self._scope(excluded_apis=["dummy.secret"])) @@ -139,7 +162,7 @@ class TestFacade: with pytest.raises(AttributeError): facade.secret() with pytest.raises(ValueError): - facade.describe_method("secret") + facade.describe_member("secret") assert "secret" not in facade.describe() assert "_internal" not in facade.describe()