22
33Backend-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
1010Data is pulled straight from `EXECUTION_CONTEXTS.current`, which is
1111populated by the time the user hits Enter because the REPL runs
1212synchronously inside `suite.run()`.
1313"""
1414
15- from typing import Any , Optional
15+ from typing import Any , List , Optional , Tuple
1616
1717from robot .running .context import EXECUTION_CONTEXTS
1818from 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+
3346def 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
0 commit comments