Skip to content

Commit 3d1665d

Browse files
committed
feat(repl): document imported resources and aliased/argument libraries in .doc and .kw
The `.doc` and `.kw` commands show documentation for the libraries, resources, and keywords you are working with in the REPL. They now cover everything the current session has imported and match how it was imported. `.doc` documents imported resource files, not just libraries — the resource introduction plus every keyword with its arguments and description. It also works for libraries that were imported with arguments, which previously could not be shown. Libraries and resources are addressed by the name they were imported under, so a library imported with an alias (`AS`, or the older `WITH NAME`) is found by that alias. `.kw` accepts the explicit `Owner.Keyword` form, e.g. `.kw BuiltIn.Log`, to pick a specific keyword when the same name is provided by more than one import; names resolve the same way they do in a suite. It now also shows full documentation for resource keywords, including the argument table. Running `.doc` or `.kw` on something you haven't imported tells you it isn't loaded instead of showing an empty page. The built-in help for both commands has been rewritten to read as user help rather than internal notes.
1 parent 73eb26a commit 3d1665d

7 files changed

Lines changed: 837 additions & 466 deletions

File tree

packages/repl/src/robotcode/repl/_keyword_lookup.py

Lines changed: 124 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
33
Backend-agnostic: both `ConsoleInterpreter` (plain) and
44
`PromptToolkitConsoleInterpreter` use these to resolve `.kw <name>`
5-
and `.doc <library>` dot-commands against the currently-imported
6-
`_kw_store`. The richer completion machinery (tokenize / candidates
7-
/ CompletionContext / Candidate) is prompt_toolkit-only and lives
8-
in `_pt.completion`.
5+
and `.doc <library-or-resource>` dot-commands against the
6+
currently-imported `_kw_store`. The richer completion machinery
7+
(tokenize / candidates / CompletionContext / Candidate) is
8+
prompt_toolkit-only and lives in `_pt.completion`.
99
1010
Data is pulled straight from `EXECUTION_CONTEXTS.current`, which is
1111
populated by the time the user hits Enter because the REPL runs
1212
synchronously inside `suite.run()`.
1313
"""
1414

15-
from typing import Any, Optional
15+
from typing import Any, List, Optional, Tuple
1616

1717
from robot.running.context import EXECUTION_CONTEXTS
1818
from robot.utils import normalize
@@ -30,6 +30,19 @@ def _norm(s: Optional[str]) -> str:
3030
return normalize(s, ignore="_") if s else ""
3131

3232

33+
def _current_kw_store() -> Optional[Any]:
34+
"""The running namespace's keyword store, or ``None`` outside a run.
35+
36+
`_kw_store` is a private attribute of Robot's `Namespace`; the
37+
``getattr`` guards the one boundary where we reach into RF
38+
internals whose shape isn't part of its public API.
39+
"""
40+
context = EXECUTION_CONTEXTS.current
41+
if context is None:
42+
return None
43+
return getattr(context.namespace, "_kw_store", None)
44+
45+
3346
def lookup_keyword_doc(name: str) -> Optional[Any]:
3447
"""Resolve ``name`` to a loaded keyword object, or ``None``.
3548
@@ -40,43 +53,127 @@ def lookup_keyword_doc(name: str) -> Optional[Any]:
4053
keyword with ``.name``, ``.args`` (``ArgumentSpec``), ``.doc``,
4154
``.tags``, ``.source``. Callers should access fields defensively.
4255
"""
43-
context = EXECUTION_CONTEXTS.current
44-
if context is None:
45-
return None
46-
store = getattr(context.namespace, "_kw_store", None)
56+
store = _current_kw_store()
4757
if store is None:
4858
return None
4959
target = _norm(name)
5060
if not target:
5161
return None
5262
for src in (*store.libraries.values(), *store.resources.values()):
53-
for kw in getattr(src, _LIB_KEYWORDS_ATTR, ()) or ():
54-
kw_name = getattr(kw, "name", None)
55-
if kw_name and _norm(kw_name) == target:
56-
return kw
63+
kw = _find_keyword_in_owner(src, target)
64+
if kw is not None:
65+
return kw
5766
return None
5867

5968

60-
def lookup_library_doc(name: str) -> Optional[Any]:
61-
"""Resolve ``name`` to a loaded library or resource, or ``None``.
69+
def lookup_keyword_owner(name: str) -> Optional[Tuple[Any, Any, bool]]:
70+
"""Resolve ``name`` to ``(owner, keyword, is_resource)``, or ``None``.
71+
72+
``owner`` is the loaded library / resource instance that defines the
73+
keyword, ``keyword`` the runtime keyword object, and ``is_resource``
74+
tells which store section it came from. Libraries are searched
75+
before resources, so library keywords win on name collisions — the
76+
same precedence as `lookup_keyword_doc`. Callers use ``owner`` to
77+
render a rich `KeywordDoc` without reimporting it from disk.
6278
63-
Case-insensitive lookup against `_kw_store`. The returned object
64-
exposes ``.name``, ``.keywords`` / ``.handlers``, ``.doc``,
65-
``.doc_format``, ``.source``. For libraries that aren't currently
66-
imported, callers can fall back to
67-
`robotcode.robot.diagnostics.library_doc.get_library_doc`.
79+
Both the plain keyword name (``Log``) and the explicit
80+
``Owner.Keyword`` form (``BuiltIn.Log``) are accepted; the plain
81+
name is tried first so a keyword whose own name contains a dot still
82+
resolves, mirroring how Robot itself dispatches keyword calls.
6883
"""
69-
context = EXECUTION_CONTEXTS.current
70-
if context is None:
84+
store = _current_kw_store()
85+
if store is None:
86+
return None
87+
target = _norm(name)
88+
if not target:
7189
return None
72-
store = getattr(context.namespace, "_kw_store", None)
90+
for owner in store.libraries.values():
91+
kw = _find_keyword_in_owner(owner, target)
92+
if kw is not None:
93+
return owner, kw, False
94+
for owner in store.resources.values():
95+
kw = _find_keyword_in_owner(owner, target)
96+
if kw is not None:
97+
return owner, kw, True
98+
if "." in name:
99+
return _lookup_explicit_keyword_owner(store, name)
100+
return None
101+
102+
103+
def _lookup_explicit_keyword_owner(store: Any, full_name: str) -> Optional[Tuple[Any, Any, bool]]:
104+
"""Resolve an explicit ``Owner.Keyword`` name against the store.
105+
106+
Every ``owner . keyword`` split of ``full_name`` is tried (so both
107+
``BuiltIn.Log`` and dotted owner/keyword names resolve), matching an
108+
owner by name in the library section before the resource section.
109+
"""
110+
for owner_name, kw_name in _owner_and_keyword_splits(full_name):
111+
owner_target = owner_name.casefold()
112+
kw_target = _norm(kw_name)
113+
for owner in store.libraries.values():
114+
if str(owner.name).casefold() == owner_target:
115+
kw = _find_keyword_in_owner(owner, kw_target)
116+
if kw is not None:
117+
return owner, kw, False
118+
for owner in store.resources.values():
119+
if str(owner.name).casefold() == owner_target:
120+
kw = _find_keyword_in_owner(owner, kw_target)
121+
if kw is not None:
122+
return owner, kw, True
123+
return None
124+
125+
126+
def _owner_and_keyword_splits(full_name: str) -> List[Tuple[str, str]]:
127+
"""``Owner.Keyword`` partitions of ``full_name``, e.g.
128+
``a.b.c`` → ``[(a, b.c), (a.b, c)]`` — same scheme Robot uses."""
129+
tokens = full_name.split(".")
130+
return [(".".join(tokens[:i]), ".".join(tokens[i:])) for i in range(1, len(tokens))]
131+
132+
133+
def _find_keyword_in_owner(owner: Any, normalized_name: str) -> Optional[Any]:
134+
"""First keyword on ``owner`` whose name folds to ``normalized_name``."""
135+
for kw in getattr(owner, _LIB_KEYWORDS_ATTR, ()) or ():
136+
kw_name = getattr(kw, "name", None)
137+
if kw_name and _norm(kw_name) == normalized_name:
138+
return kw
139+
return None
140+
141+
142+
def lookup_library(name: str) -> Optional[Any]:
143+
"""The loaded library instance named ``name``, or ``None``.
144+
145+
Looks only in the store's library section, so the result is always
146+
a Robot `TestLibrary` (RF >= 7) / `_BaseTestLibrary` (RF < 7) —
147+
suitable for
148+
`robotcode.robot.diagnostics.library_doc.get_library_doc_from_library`.
149+
"""
150+
store = _current_kw_store()
151+
if store is None:
152+
return None
153+
return _find_owner_by_name(store.libraries.values(), name)
154+
155+
156+
def lookup_resource(name: str) -> Optional[Any]:
157+
"""The loaded resource instance named ``name``, or ``None``.
158+
159+
Looks only in the store's resource section, so the result is a
160+
`UserLibrary` (RF < 7) / running `ResourceFile` (RF >= 7) —
161+
suitable for
162+
`robotcode.robot.diagnostics.library_doc.get_resource_doc_from_resource`.
163+
"""
164+
store = _current_kw_store()
73165
if store is None:
74166
return None
167+
return _find_owner_by_name(store.resources.values(), name)
168+
169+
170+
def _find_owner_by_name(owners: Any, name: str) -> Optional[Any]:
171+
"""First owner in ``owners`` whose ``.name`` case-folds to ``name``."""
75172
target = name.casefold()
76173
if not target:
77174
return None
78-
for src in (*store.libraries.values(), *store.resources.values()):
79-
src_name = getattr(src, "name", None)
80-
if src_name and str(src_name).casefold() == target:
81-
return src
175+
for owner in owners:
176+
owner_name = owner.name
177+
if owner_name and str(owner_name).casefold() == target:
178+
return owner
82179
return None

packages/repl/src/robotcode/repl/_pt/completion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
handlers in `packages/language_server/.../parts/completion.py`.
1515
1616
Robot-runtime lookup helpers used by both interpreters
17-
(`lookup_keyword_doc`, `lookup_library_doc`) live in
17+
(`lookup_keyword_doc`, `lookup_library`, `lookup_resource`) live in
1818
`robotcode.repl._keyword_lookup` — those don't depend on
1919
prompt_toolkit and shouldn't import this module.
2020
"""

0 commit comments

Comments
 (0)