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()