##// END OF EJS Templates
Merge pull request #13745 from krassowski/completion-matcher...
Matthias Bussonnier -
r27811:dc08a337 merge
parent child Browse files
Show More
@@ -0,0 +1,3 b''
1 import os
2
3 GENERATING_DOCUMENTATION = os.environ.get("IN_SPHINX_RUN", None) == "True"
This diff has been collapsed as it changes many lines, (973 lines changed) Show them Hide them
@@ -100,6 +100,73 b' option.'
100
100
101 Be sure to update :any:`jedi` to the latest stable version or to try the
101 Be sure to update :any:`jedi` to the latest stable version or to try the
102 current development version to get better completions.
102 current development version to get better completions.
103
104 Matchers
105 ========
106
107 All completions routines are implemented using unified *Matchers* API.
108 The matchers API is provisional and subject to change without notice.
109
110 The built-in matchers include:
111
112 - :any:`IPCompleter.dict_key_matcher`: dictionary key completions,
113 - :any:`IPCompleter.magic_matcher`: completions for magics,
114 - :any:`IPCompleter.unicode_name_matcher`,
115 :any:`IPCompleter.fwd_unicode_matcher`
116 and :any:`IPCompleter.latex_name_matcher`: see `Forward latex/unicode completion`_,
117 - :any:`back_unicode_name_matcher` and :any:`back_latex_name_matcher`: see `Backward latex completion`_,
118 - :any:`IPCompleter.file_matcher`: paths to files and directories,
119 - :any:`IPCompleter.python_func_kw_matcher` - function keywords,
120 - :any:`IPCompleter.python_matches` - globals and attributes (v1 API),
121 - ``IPCompleter.jedi_matcher`` - static analysis with Jedi,
122 - :any:`IPCompleter.custom_completer_matcher` - pluggable completer with a default
123 implementation in :any:`InteractiveShell` which uses IPython hooks system
124 (`complete_command`) with string dispatch (including regular expressions).
125 Differently to other matchers, ``custom_completer_matcher`` will not suppress
126 Jedi results to match behaviour in earlier IPython versions.
127
128 Custom matchers can be added by appending to ``IPCompleter.custom_matchers`` list.
129
130 Matcher API
131 -----------
132
133 Simplifying some details, the ``Matcher`` interface can described as
134
135 .. code-block::
136
137 MatcherAPIv1 = Callable[[str], list[str]]
138 MatcherAPIv2 = Callable[[CompletionContext], SimpleMatcherResult]
139
140 Matcher = MatcherAPIv1 | MatcherAPIv2
141
142 The ``MatcherAPIv1`` reflects the matcher API as available prior to IPython 8.6.0
143 and remains supported as a simplest way for generating completions. This is also
144 currently the only API supported by the IPython hooks system `complete_command`.
145
146 To distinguish between matcher versions ``matcher_api_version`` attribute is used.
147 More precisely, the API allows to omit ``matcher_api_version`` for v1 Matchers,
148 and requires a literal ``2`` for v2 Matchers.
149
150 Once the API stabilises future versions may relax the requirement for specifying
151 ``matcher_api_version`` by switching to :any:`functools.singledispatch`, therefore
152 please do not rely on the presence of ``matcher_api_version`` for any purposes.
153
154 Suppression of competing matchers
155 ---------------------------------
156
157 By default results from all matchers are combined, in the order determined by
158 their priority. Matchers can request to suppress results from subsequent
159 matchers by setting ``suppress`` to ``True`` in the ``MatcherResult``.
160
161 When multiple matchers simultaneously request surpression, the results from of
162 the matcher with higher priority will be returned.
163
164 Sometimes it is desirable to suppress most but not all other matchers;
165 this can be achieved by adding a list of identifiers of matchers which
166 should not be suppressed to ``MatcherResult`` under ``do_not_suppress`` key.
167
168 The suppression behaviour can is user-configurable via
169 :any:`IPCompleter.suppress_competing_matchers`.
103 """
170 """
104
171
105
172
@@ -109,7 +176,7 b' current development version to get better completions.'
109 # Some of this code originated from rlcompleter in the Python standard library
176 # Some of this code originated from rlcompleter in the Python standard library
110 # Copyright (C) 2001 Python Software Foundation, www.python.org
177 # Copyright (C) 2001 Python Software Foundation, www.python.org
111
178
112
179 from __future__ import annotations
113 import builtins as builtin_mod
180 import builtins as builtin_mod
114 import glob
181 import glob
115 import inspect
182 import inspect
@@ -124,9 +191,26 b' import unicodedata'
124 import uuid
191 import uuid
125 import warnings
192 import warnings
126 from contextlib import contextmanager
193 from contextlib import contextmanager
194 from dataclasses import dataclass
195 from functools import cached_property, partial
127 from importlib import import_module
196 from importlib import import_module
128 from types import SimpleNamespace
197 from types import SimpleNamespace
129 from typing import Iterable, Iterator, List, Tuple, Union, Any, Sequence, Dict, NamedTuple, Pattern, Optional
198 from typing import (
199 Iterable,
200 Iterator,
201 List,
202 Tuple,
203 Union,
204 Any,
205 Sequence,
206 Dict,
207 NamedTuple,
208 Pattern,
209 Optional,
210 TYPE_CHECKING,
211 Set,
212 Literal,
213 )
130
214
131 from IPython.core.error import TryNext
215 from IPython.core.error import TryNext
132 from IPython.core.inputtransformer2 import ESC_MAGIC
216 from IPython.core.inputtransformer2 import ESC_MAGIC
@@ -134,10 +218,22 b' from IPython.core.latex_symbols import latex_symbols, reverse_latex_symbol'
134 from IPython.core.oinspect import InspectColors
218 from IPython.core.oinspect import InspectColors
135 from IPython.testing.skipdoctest import skip_doctest
219 from IPython.testing.skipdoctest import skip_doctest
136 from IPython.utils import generics
220 from IPython.utils import generics
221 from IPython.utils.decorators import sphinx_options
137 from IPython.utils.dir2 import dir2, get_real_method
222 from IPython.utils.dir2 import dir2, get_real_method
223 from IPython.utils.docs import GENERATING_DOCUMENTATION
138 from IPython.utils.path import ensure_dir_exists
224 from IPython.utils.path import ensure_dir_exists
139 from IPython.utils.process import arg_split
225 from IPython.utils.process import arg_split
140 from traitlets import Bool, Enum, Int, List as ListTrait, Unicode, default, observe
226 from traitlets import (
227 Bool,
228 Enum,
229 Int,
230 List as ListTrait,
231 Unicode,
232 Dict as DictTrait,
233 Union as UnionTrait,
234 default,
235 observe,
236 )
141 from traitlets.config.configurable import Configurable
237 from traitlets.config.configurable import Configurable
142
238
143 import __main__
239 import __main__
@@ -145,6 +241,7 b' import __main__'
145 # skip module docstests
241 # skip module docstests
146 __skip_doctest__ = True
242 __skip_doctest__ = True
147
243
244
148 try:
245 try:
149 import jedi
246 import jedi
150 jedi.settings.case_insensitive_completion = False
247 jedi.settings.case_insensitive_completion = False
@@ -153,7 +250,26 b' try:'
153 JEDI_INSTALLED = True
250 JEDI_INSTALLED = True
154 except ImportError:
251 except ImportError:
155 JEDI_INSTALLED = False
252 JEDI_INSTALLED = False
156 #-----------------------------------------------------------------------------
253
254
255 if TYPE_CHECKING or GENERATING_DOCUMENTATION:
256 from typing import cast
257 from typing_extensions import TypedDict, NotRequired, Protocol, TypeAlias
258 else:
259
260 def cast(obj, type_):
261 """Workaround for `TypeError: MatcherAPIv2() takes no arguments`"""
262 return obj
263
264 # do not require on runtime
265 NotRequired = Tuple # requires Python >=3.11
266 TypedDict = Dict # by extension of `NotRequired` requires 3.11 too
267 Protocol = object # requires Python >=3.8
268 TypeAlias = Any # requires Python >=3.10
269 if GENERATING_DOCUMENTATION:
270 from typing import TypedDict
271
272 # -----------------------------------------------------------------------------
157 # Globals
273 # Globals
158 #-----------------------------------------------------------------------------
274 #-----------------------------------------------------------------------------
159
275
@@ -166,7 +282,7 b' except ImportError:'
166 _UNICODE_RANGES = [(32, 0x3134b), (0xe0001, 0xe01f0)]
282 _UNICODE_RANGES = [(32, 0x3134b), (0xe0001, 0xe01f0)]
167
283
168 # Public API
284 # Public API
169 __all__ = ['Completer','IPCompleter']
285 __all__ = ["Completer", "IPCompleter"]
170
286
171 if sys.platform == 'win32':
287 if sys.platform == 'win32':
172 PROTECTABLES = ' '
288 PROTECTABLES = ' '
@@ -177,6 +293,8 b' else:'
177 # may have trouble processing.
293 # may have trouble processing.
178 MATCHES_LIMIT = 500
294 MATCHES_LIMIT = 500
179
295
296 # Completion type reported when no type can be inferred.
297 _UNKNOWN_TYPE = "<unknown>"
180
298
181 class ProvisionalCompleterWarning(FutureWarning):
299 class ProvisionalCompleterWarning(FutureWarning):
182 """
300 """
@@ -355,9 +473,12 b' class _FakeJediCompletion:'
355 return '<Fake completion object jedi has crashed>'
473 return '<Fake completion object jedi has crashed>'
356
474
357
475
476 _JediCompletionLike = Union[jedi.api.Completion, _FakeJediCompletion]
477
478
358 class Completion:
479 class Completion:
359 """
480 """
360 Completion object used and return by IPython completers.
481 Completion object used and returned by IPython completers.
361
482
362 .. warning::
483 .. warning::
363
484
@@ -417,6 +538,188 b' class Completion:'
417 return hash((self.start, self.end, self.text))
538 return hash((self.start, self.end, self.text))
418
539
419
540
541 class SimpleCompletion:
542 """Completion item to be included in the dictionary returned by new-style Matcher (API v2).
543
544 .. warning::
545
546 Provisional
547
548 This class is used to describe the currently supported attributes of
549 simple completion items, and any additional implementation details
550 should not be relied on. Additional attributes may be included in
551 future versions, and meaning of text disambiguated from the current
552 dual meaning of "text to insert" and "text to used as a label".
553 """
554
555 __slots__ = ["text", "type"]
556
557 def __init__(self, text: str, *, type: str = None):
558 self.text = text
559 self.type = type
560
561 def __repr__(self):
562 return f"<SimpleCompletion text={self.text!r} type={self.type!r}>"
563
564
565 class _MatcherResultBase(TypedDict):
566 """Definition of dictionary to be returned by new-style Matcher (API v2)."""
567
568 #: Suffix of the provided ``CompletionContext.token``, if not given defaults to full token.
569 matched_fragment: NotRequired[str]
570
571 #: Whether to suppress results from all other matchers (True), some
572 #: matchers (set of identifiers) or none (False); default is False.
573 suppress: NotRequired[Union[bool, Set[str]]]
574
575 #: Identifiers of matchers which should NOT be suppressed when this matcher
576 #: requests to suppress all other matchers; defaults to an empty set.
577 do_not_suppress: NotRequired[Set[str]]
578
579 #: Are completions already ordered and should be left as-is? default is False.
580 ordered: NotRequired[bool]
581
582
583 @sphinx_options(show_inherited_members=True, exclude_inherited_from=["dict"])
584 class SimpleMatcherResult(_MatcherResultBase, TypedDict):
585 """Result of new-style completion matcher."""
586
587 # note: TypedDict is added again to the inheritance chain
588 # in order to get __orig_bases__ for documentation
589
590 #: List of candidate completions
591 completions: Sequence[SimpleCompletion]
592
593
594 class _JediMatcherResult(_MatcherResultBase):
595 """Matching result returned by Jedi (will be processed differently)"""
596
597 #: list of candidate completions
598 completions: Iterable[_JediCompletionLike]
599
600
601 @dataclass
602 class CompletionContext:
603 """Completion context provided as an argument to matchers in the Matcher API v2."""
604
605 # rationale: many legacy matchers relied on completer state (`self.text_until_cursor`)
606 # which was not explicitly visible as an argument of the matcher, making any refactor
607 # prone to errors; by explicitly passing `cursor_position` we can decouple the matchers
608 # from the completer, and make substituting them in sub-classes easier.
609
610 #: Relevant fragment of code directly preceding the cursor.
611 #: The extraction of token is implemented via splitter heuristic
612 #: (following readline behaviour for legacy reasons), which is user configurable
613 #: (by switching the greedy mode).
614 token: str
615
616 #: The full available content of the editor or buffer
617 full_text: str
618
619 #: Cursor position in the line (the same for ``full_text`` and ``text``).
620 cursor_position: int
621
622 #: Cursor line in ``full_text``.
623 cursor_line: int
624
625 #: The maximum number of completions that will be used downstream.
626 #: Matchers can use this information to abort early.
627 #: The built-in Jedi matcher is currently excepted from this limit.
628 # If not given, return all possible completions.
629 limit: Optional[int]
630
631 @cached_property
632 def text_until_cursor(self) -> str:
633 return self.line_with_cursor[: self.cursor_position]
634
635 @cached_property
636 def line_with_cursor(self) -> str:
637 return self.full_text.split("\n")[self.cursor_line]
638
639
640 #: Matcher results for API v2.
641 MatcherResult = Union[SimpleMatcherResult, _JediMatcherResult]
642
643
644 class _MatcherAPIv1Base(Protocol):
645 def __call__(self, text: str) -> list[str]:
646 """Call signature."""
647
648
649 class _MatcherAPIv1Total(_MatcherAPIv1Base, Protocol):
650 #: API version
651 matcher_api_version: Optional[Literal[1]]
652
653 def __call__(self, text: str) -> list[str]:
654 """Call signature."""
655
656
657 #: Protocol describing Matcher API v1.
658 MatcherAPIv1: TypeAlias = Union[_MatcherAPIv1Base, _MatcherAPIv1Total]
659
660
661 class MatcherAPIv2(Protocol):
662 """Protocol describing Matcher API v2."""
663
664 #: API version
665 matcher_api_version: Literal[2] = 2
666
667 def __call__(self, context: CompletionContext) -> MatcherResult:
668 """Call signature."""
669
670
671 Matcher: TypeAlias = Union[MatcherAPIv1, MatcherAPIv2]
672
673
674 def completion_matcher(
675 *, priority: float = None, identifier: str = None, api_version: int = 1
676 ):
677 """Adds attributes describing the matcher.
678
679 Parameters
680 ----------
681 priority : Optional[float]
682 The priority of the matcher, determines the order of execution of matchers.
683 Higher priority means that the matcher will be executed first. Defaults to 0.
684 identifier : Optional[str]
685 identifier of the matcher allowing users to modify the behaviour via traitlets,
686 and also used to for debugging (will be passed as ``origin`` with the completions).
687 Defaults to matcher function ``__qualname__``.
688 api_version: Optional[int]
689 version of the Matcher API used by this matcher.
690 Currently supported values are 1 and 2.
691 Defaults to 1.
692 """
693
694 def wrapper(func: Matcher):
695 func.matcher_priority = priority or 0
696 func.matcher_identifier = identifier or func.__qualname__
697 func.matcher_api_version = api_version
698 if TYPE_CHECKING:
699 if api_version == 1:
700 func = cast(func, MatcherAPIv1)
701 elif api_version == 2:
702 func = cast(func, MatcherAPIv2)
703 return func
704
705 return wrapper
706
707
708 def _get_matcher_priority(matcher: Matcher):
709 return getattr(matcher, "matcher_priority", 0)
710
711
712 def _get_matcher_id(matcher: Matcher):
713 return getattr(matcher, "matcher_identifier", matcher.__qualname__)
714
715
716 def _get_matcher_api_version(matcher):
717 return getattr(matcher, "matcher_api_version", 1)
718
719
720 context_matcher = partial(completion_matcher, api_version=2)
721
722
420 _IC = Iterable[Completion]
723 _IC = Iterable[Completion]
421
724
422
725
@@ -924,7 +1227,20 b' def _safe_isinstance(obj, module, class_name):'
924 return (module in sys.modules and
1227 return (module in sys.modules and
925 isinstance(obj, getattr(import_module(module), class_name)))
1228 isinstance(obj, getattr(import_module(module), class_name)))
926
1229
927 def back_unicode_name_matches(text:str) -> Tuple[str, Sequence[str]]:
1230
1231 @context_matcher()
1232 def back_unicode_name_matcher(context: CompletionContext):
1233 """Match Unicode characters back to Unicode name
1234
1235 Same as :any:`back_unicode_name_matches`, but adopted to new Matcher API.
1236 """
1237 fragment, matches = back_unicode_name_matches(context.text_until_cursor)
1238 return _convert_matcher_v1_result_to_v2(
1239 matches, type="unicode", fragment=fragment, suppress_if_matches=True
1240 )
1241
1242
1243 def back_unicode_name_matches(text: str) -> Tuple[str, Sequence[str]]:
928 """Match Unicode characters back to Unicode name
1244 """Match Unicode characters back to Unicode name
929
1245
930 This does ``☃`` -> ``\\snowman``
1246 This does ``☃`` -> ``\\snowman``
@@ -934,6 +1250,9 b' def back_unicode_name_matches(text:str) -> Tuple[str, Sequence[str]]:'
934
1250
935 This will not either back-complete standard sequences like \\n, \\b ...
1251 This will not either back-complete standard sequences like \\n, \\b ...
936
1252
1253 .. deprecated:: 8.6
1254 You can use :meth:`back_unicode_name_matcher` instead.
1255
937 Returns
1256 Returns
938 =======
1257 =======
939
1258
@@ -943,7 +1262,6 b' def back_unicode_name_matches(text:str) -> Tuple[str, Sequence[str]]:'
943 empty string,
1262 empty string,
944 - a sequence (of 1), name for the match Unicode character, preceded by
1263 - a sequence (of 1), name for the match Unicode character, preceded by
945 backslash, or empty if no match.
1264 backslash, or empty if no match.
946
947 """
1265 """
948 if len(text)<2:
1266 if len(text)<2:
949 return '', ()
1267 return '', ()
@@ -963,11 +1281,26 b' def back_unicode_name_matches(text:str) -> Tuple[str, Sequence[str]]:'
963 pass
1281 pass
964 return '', ()
1282 return '', ()
965
1283
966 def back_latex_name_matches(text:str) -> Tuple[str, Sequence[str]] :
1284
1285 @context_matcher()
1286 def back_latex_name_matcher(context: CompletionContext):
1287 """Match latex characters back to unicode name
1288
1289 Same as :any:`back_latex_name_matches`, but adopted to new Matcher API.
1290 """
1291 fragment, matches = back_latex_name_matches(context.text_until_cursor)
1292 return _convert_matcher_v1_result_to_v2(
1293 matches, type="latex", fragment=fragment, suppress_if_matches=True
1294 )
1295
1296
1297 def back_latex_name_matches(text: str) -> Tuple[str, Sequence[str]]:
967 """Match latex characters back to unicode name
1298 """Match latex characters back to unicode name
968
1299
969 This does ``\\ℵ`` -> ``\\aleph``
1300 This does ``\\ℵ`` -> ``\\aleph``
970
1301
1302 .. deprecated:: 8.6
1303 You can use :meth:`back_latex_name_matcher` instead.
971 """
1304 """
972 if len(text)<2:
1305 if len(text)<2:
973 return '', ()
1306 return '', ()
@@ -1042,11 +1375,23 b' def _make_signature(completion)-> str:'
1042 for p in signature.defined_names()) if f])
1375 for p in signature.defined_names()) if f])
1043
1376
1044
1377
1045 class _CompleteResult(NamedTuple):
1378 _CompleteResult = Dict[str, MatcherResult]
1046 matched_text : str
1379
1047 matches: Sequence[str]
1380
1048 matches_origin: Sequence[str]
1381 def _convert_matcher_v1_result_to_v2(
1049 jedi_matches: Any
1382 matches: Sequence[str],
1383 type: str,
1384 fragment: str = None,
1385 suppress_if_matches: bool = False,
1386 ) -> SimpleMatcherResult:
1387 """Utility to help with transition"""
1388 result = {
1389 "completions": [SimpleCompletion(text=match, type=type) for match in matches],
1390 "suppress": (True if matches else False) if suppress_if_matches else False,
1391 }
1392 if fragment is not None:
1393 result["matched_fragment"] = fragment
1394 return result
1050
1395
1051
1396
1052 class IPCompleter(Completer):
1397 class IPCompleter(Completer):
@@ -1062,17 +1407,59 b' class IPCompleter(Completer):'
1062 else:
1407 else:
1063 self.splitter.delims = DELIMS
1408 self.splitter.delims = DELIMS
1064
1409
1065 dict_keys_only = Bool(False,
1410 dict_keys_only = Bool(
1066 help="""Whether to show dict key matches only""")
1411 False,
1412 help="""
1413 Whether to show dict key matches only.
1414
1415 (disables all matchers except for `IPCompleter.dict_key_matcher`).
1416 """,
1417 )
1418
1419 suppress_competing_matchers = UnionTrait(
1420 [Bool(allow_none=True), DictTrait(Bool(None, allow_none=True))],
1421 default_value=None,
1422 help="""
1423 Whether to suppress completions from other *Matchers*.
1424
1425 When set to ``None`` (default) the matchers will attempt to auto-detect
1426 whether suppression of other matchers is desirable. For example, at
1427 the beginning of a line followed by `%` we expect a magic completion
1428 to be the only applicable option, and after ``my_dict['`` we usually
1429 expect a completion with an existing dictionary key.
1430
1431 If you want to disable this heuristic and see completions from all matchers,
1432 set ``IPCompleter.suppress_competing_matchers = False``.
1433 To disable the heuristic for specific matchers provide a dictionary mapping:
1434 ``IPCompleter.suppress_competing_matchers = {'IPCompleter.dict_key_matcher': False}``.
1435
1436 Set ``IPCompleter.suppress_competing_matchers = True`` to limit
1437 completions to the set of matchers with the highest priority;
1438 this is equivalent to ``IPCompleter.merge_completions`` and
1439 can be beneficial for performance, but will sometimes omit relevant
1440 candidates from matchers further down the priority list.
1441 """,
1442 ).tag(config=True)
1067
1443
1068 merge_completions = Bool(True,
1444 merge_completions = Bool(
1445 True,
1069 help="""Whether to merge completion results into a single list
1446 help="""Whether to merge completion results into a single list
1070
1447
1071 If False, only the completion results from the first non-empty
1448 If False, only the completion results from the first non-empty
1072 completer will be returned.
1449 completer will be returned.
1073 """
1450
1451 As of version 8.6.0, setting the value to ``False`` is an alias for:
1452 ``IPCompleter.suppress_competing_matchers = True.``.
1453 """,
1454 ).tag(config=True)
1455
1456 disable_matchers = ListTrait(
1457 Unicode(), help="""List of matchers to disable."""
1074 ).tag(config=True)
1458 ).tag(config=True)
1075 omit__names = Enum((0,1,2), default_value=2,
1459
1460 omit__names = Enum(
1461 (0, 1, 2),
1462 default_value=2,
1076 help="""Instruct the completer to omit private method names
1463 help="""Instruct the completer to omit private method names
1077
1464
1078 Specifically, when completing on ``object.<tab>``.
1465 Specifically, when completing on ``object.<tab>``.
@@ -1148,7 +1535,7 b' class IPCompleter(Completer):'
1148 namespace=namespace,
1535 namespace=namespace,
1149 global_namespace=global_namespace,
1536 global_namespace=global_namespace,
1150 config=config,
1537 config=config,
1151 **kwargs
1538 **kwargs,
1152 )
1539 )
1153
1540
1154 # List where completion matches will be stored
1541 # List where completion matches will be stored
@@ -1177,8 +1564,8 b' class IPCompleter(Completer):'
1177 #= re.compile(r'[\s|\[]*(\w+)(?:\s*=?\s*.*)')
1564 #= re.compile(r'[\s|\[]*(\w+)(?:\s*=?\s*.*)')
1178
1565
1179 self.magic_arg_matchers = [
1566 self.magic_arg_matchers = [
1180 self.magic_config_matches,
1567 self.magic_config_matcher,
1181 self.magic_color_matches,
1568 self.magic_color_matcher,
1182 ]
1569 ]
1183
1570
1184 # This is set externally by InteractiveShell
1571 # This is set externally by InteractiveShell
@@ -1190,27 +1577,50 b' class IPCompleter(Completer):'
1190 # attribute through the `@unicode_names` property.
1577 # attribute through the `@unicode_names` property.
1191 self._unicode_names = None
1578 self._unicode_names = None
1192
1579
1580 self._backslash_combining_matchers = [
1581 self.latex_name_matcher,
1582 self.unicode_name_matcher,
1583 back_latex_name_matcher,
1584 back_unicode_name_matcher,
1585 self.fwd_unicode_matcher,
1586 ]
1587
1588 if not self.backslash_combining_completions:
1589 for matcher in self._backslash_combining_matchers:
1590 self.disable_matchers.append(matcher.matcher_identifier)
1591
1592 if not self.merge_completions:
1593 self.suppress_competing_matchers = True
1594
1193 @property
1595 @property
1194 def matchers(self) -> List[Any]:
1596 def matchers(self) -> List[Matcher]:
1195 """All active matcher routines for completion"""
1597 """All active matcher routines for completion"""
1196 if self.dict_keys_only:
1598 if self.dict_keys_only:
1197 return [self.dict_key_matches]
1599 return [self.dict_key_matcher]
1198
1600
1199 if self.use_jedi:
1601 if self.use_jedi:
1200 return [
1602 return [
1201 *self.custom_matchers,
1603 *self.custom_matchers,
1202 self.dict_key_matches,
1604 *self._backslash_combining_matchers,
1203 self.file_matches,
1605 *self.magic_arg_matchers,
1204 self.magic_matches,
1606 self.custom_completer_matcher,
1607 self.magic_matcher,
1608 self._jedi_matcher,
1609 self.dict_key_matcher,
1610 self.file_matcher,
1205 ]
1611 ]
1206 else:
1612 else:
1207 return [
1613 return [
1208 *self.custom_matchers,
1614 *self.custom_matchers,
1209 self.dict_key_matches,
1615 *self._backslash_combining_matchers,
1616 *self.magic_arg_matchers,
1617 self.custom_completer_matcher,
1618 self.dict_key_matcher,
1619 # TODO: convert python_matches to v2 API
1620 self.magic_matcher,
1210 self.python_matches,
1621 self.python_matches,
1211 self.file_matches,
1622 self.file_matcher,
1212 self.magic_matches,
1623 self.python_func_kw_matcher,
1213 self.python_func_kw_matches,
1214 ]
1624 ]
1215
1625
1216 def all_completions(self, text:str) -> List[str]:
1626 def all_completions(self, text:str) -> List[str]:
@@ -1231,7 +1641,15 b' class IPCompleter(Completer):'
1231 return [f.replace("\\","/")
1641 return [f.replace("\\","/")
1232 for f in self.glob("%s*" % text)]
1642 for f in self.glob("%s*" % text)]
1233
1643
1234 def file_matches(self, text:str)->List[str]:
1644 @context_matcher()
1645 def file_matcher(self, context: CompletionContext) -> SimpleMatcherResult:
1646 """Same as :any:`file_matches`, but adopted to new Matcher API."""
1647 matches = self.file_matches(context.token)
1648 # TODO: add a heuristic for suppressing (e.g. if it has OS-specific delimiter,
1649 # starts with `/home/`, `C:\`, etc)
1650 return _convert_matcher_v1_result_to_v2(matches, type="path")
1651
1652 def file_matches(self, text: str) -> List[str]:
1235 """Match filenames, expanding ~USER type strings.
1653 """Match filenames, expanding ~USER type strings.
1236
1654
1237 Most of the seemingly convoluted logic in this completer is an
1655 Most of the seemingly convoluted logic in this completer is an
@@ -1243,7 +1661,11 b' class IPCompleter(Completer):'
1243 only the parts after what's already been typed (instead of the
1661 only the parts after what's already been typed (instead of the
1244 full completions, as is normally done). I don't think with the
1662 full completions, as is normally done). I don't think with the
1245 current (as of Python 2.3) Python readline it's possible to do
1663 current (as of Python 2.3) Python readline it's possible to do
1246 better."""
1664 better.
1665
1666 .. deprecated:: 8.6
1667 You can use :meth:`file_matcher` instead.
1668 """
1247
1669
1248 # chars that require escaping with backslash - i.e. chars
1670 # chars that require escaping with backslash - i.e. chars
1249 # that readline treats incorrectly as delimiters, but we
1671 # that readline treats incorrectly as delimiters, but we
@@ -1313,8 +1735,22 b' class IPCompleter(Completer):'
1313 # Mark directories in input list by appending '/' to their names.
1735 # Mark directories in input list by appending '/' to their names.
1314 return [x+'/' if os.path.isdir(x) else x for x in matches]
1736 return [x+'/' if os.path.isdir(x) else x for x in matches]
1315
1737
1316 def magic_matches(self, text:str):
1738 @context_matcher()
1317 """Match magics"""
1739 def magic_matcher(self, context: CompletionContext) -> SimpleMatcherResult:
1740 """Match magics."""
1741 text = context.token
1742 matches = self.magic_matches(text)
1743 result = _convert_matcher_v1_result_to_v2(matches, type="magic")
1744 is_magic_prefix = len(text) > 0 and text[0] == "%"
1745 result["suppress"] = is_magic_prefix and bool(result["completions"])
1746 return result
1747
1748 def magic_matches(self, text: str):
1749 """Match magics.
1750
1751 .. deprecated:: 8.6
1752 You can use :meth:`magic_matcher` instead.
1753 """
1318 # Get all shell magics now rather than statically, so magics loaded at
1754 # Get all shell magics now rather than statically, so magics loaded at
1319 # runtime show up too.
1755 # runtime show up too.
1320 lsm = self.shell.magics_manager.lsmagic()
1756 lsm = self.shell.magics_manager.lsmagic()
@@ -1355,8 +1791,19 b' class IPCompleter(Completer):'
1355
1791
1356 return comp
1792 return comp
1357
1793
1358 def magic_config_matches(self, text:str) -> List[str]:
1794 @context_matcher()
1359 """ Match class names and attributes for %config magic """
1795 def magic_config_matcher(self, context: CompletionContext) -> SimpleMatcherResult:
1796 """Match class names and attributes for %config magic."""
1797 # NOTE: uses `line_buffer` equivalent for compatibility
1798 matches = self.magic_config_matches(context.line_with_cursor)
1799 return _convert_matcher_v1_result_to_v2(matches, type="param")
1800
1801 def magic_config_matches(self, text: str) -> List[str]:
1802 """Match class names and attributes for %config magic.
1803
1804 .. deprecated:: 8.6
1805 You can use :meth:`magic_config_matcher` instead.
1806 """
1360 texts = text.strip().split()
1807 texts = text.strip().split()
1361
1808
1362 if len(texts) > 0 and (texts[0] == 'config' or texts[0] == '%config'):
1809 if len(texts) > 0 and (texts[0] == 'config' or texts[0] == '%config'):
@@ -1390,8 +1837,19 b' class IPCompleter(Completer):'
1390 if attr.startswith(texts[1]) ]
1837 if attr.startswith(texts[1]) ]
1391 return []
1838 return []
1392
1839
1393 def magic_color_matches(self, text:str) -> List[str] :
1840 @context_matcher()
1394 """ Match color schemes for %colors magic"""
1841 def magic_color_matcher(self, context: CompletionContext) -> SimpleMatcherResult:
1842 """Match color schemes for %colors magic."""
1843 # NOTE: uses `line_buffer` equivalent for compatibility
1844 matches = self.magic_color_matches(context.line_with_cursor)
1845 return _convert_matcher_v1_result_to_v2(matches, type="param")
1846
1847 def magic_color_matches(self, text: str) -> List[str]:
1848 """Match color schemes for %colors magic.
1849
1850 .. deprecated:: 8.6
1851 You can use :meth:`magic_color_matcher` instead.
1852 """
1395 texts = text.split()
1853 texts = text.split()
1396 if text.endswith(' '):
1854 if text.endswith(' '):
1397 # .split() strips off the trailing whitespace. Add '' back
1855 # .split() strips off the trailing whitespace. Add '' back
@@ -1404,9 +1862,24 b' class IPCompleter(Completer):'
1404 if color.startswith(prefix) ]
1862 if color.startswith(prefix) ]
1405 return []
1863 return []
1406
1864
1407 def _jedi_matches(self, cursor_column:int, cursor_line:int, text:str) -> Iterable[Any]:
1865 @context_matcher(identifier="IPCompleter.jedi_matcher")
1866 def _jedi_matcher(self, context: CompletionContext) -> _JediMatcherResult:
1867 matches = self._jedi_matches(
1868 cursor_column=context.cursor_position,
1869 cursor_line=context.cursor_line,
1870 text=context.full_text,
1871 )
1872 return {
1873 "completions": matches,
1874 # static analysis should not suppress other matchers
1875 "suppress": False,
1876 }
1877
1878 def _jedi_matches(
1879 self, cursor_column: int, cursor_line: int, text: str
1880 ) -> Iterable[_JediCompletionLike]:
1408 """
1881 """
1409 Return a list of :any:`jedi.api.Completions` object from a ``text`` and
1882 Return a list of :any:`jedi.api.Completion`s object from a ``text`` and
1410 cursor position.
1883 cursor position.
1411
1884
1412 Parameters
1885 Parameters
@@ -1422,6 +1895,9 b' class IPCompleter(Completer):'
1422 -----
1895 -----
1423 If ``IPCompleter.debug`` is ``True`` may return a :any:`_FakeJediCompletion`
1896 If ``IPCompleter.debug`` is ``True`` may return a :any:`_FakeJediCompletion`
1424 object containing a string with the Jedi debug information attached.
1897 object containing a string with the Jedi debug information attached.
1898
1899 .. deprecated:: 8.6
1900 You can use :meth:`_jedi_matcher` instead.
1425 """
1901 """
1426 namespaces = [self.namespace]
1902 namespaces = [self.namespace]
1427 if self.global_namespace is not None:
1903 if self.global_namespace is not None:
@@ -1558,8 +2034,18 b' class IPCompleter(Completer):'
1558
2034
1559 return list(set(ret))
2035 return list(set(ret))
1560
2036
2037 @context_matcher()
2038 def python_func_kw_matcher(self, context: CompletionContext) -> SimpleMatcherResult:
2039 """Match named parameters (kwargs) of the last open function."""
2040 matches = self.python_func_kw_matches(context.token)
2041 return _convert_matcher_v1_result_to_v2(matches, type="param")
2042
1561 def python_func_kw_matches(self, text):
2043 def python_func_kw_matches(self, text):
1562 """Match named parameters (kwargs) of the last open function"""
2044 """Match named parameters (kwargs) of the last open function.
2045
2046 .. deprecated:: 8.6
2047 You can use :meth:`python_func_kw_matcher` instead.
2048 """
1563
2049
1564 if "." in text: # a parameter cannot be dotted
2050 if "." in text: # a parameter cannot be dotted
1565 return []
2051 return []
@@ -1654,9 +2140,20 b' class IPCompleter(Completer):'
1654 return obj.dtype.names or []
2140 return obj.dtype.names or []
1655 return []
2141 return []
1656
2142
1657 def dict_key_matches(self, text:str) -> List[str]:
2143 @context_matcher()
1658 "Match string keys in a dictionary, after e.g. 'foo[' "
2144 def dict_key_matcher(self, context: CompletionContext) -> SimpleMatcherResult:
2145 """Match string keys in a dictionary, after e.g. ``foo[``."""
2146 matches = self.dict_key_matches(context.token)
2147 return _convert_matcher_v1_result_to_v2(
2148 matches, type="dict key", suppress_if_matches=True
2149 )
2150
2151 def dict_key_matches(self, text: str) -> List[str]:
2152 """Match string keys in a dictionary, after e.g. ``foo[``.
1659
2153
2154 .. deprecated:: 8.6
2155 You can use :meth:`dict_key_matcher` instead.
2156 """
1660
2157
1661 if self.__dict_key_regexps is not None:
2158 if self.__dict_key_regexps is not None:
1662 regexps = self.__dict_key_regexps
2159 regexps = self.__dict_key_regexps
@@ -1758,8 +2255,16 b' class IPCompleter(Completer):'
1758
2255
1759 return [leading + k + suf for k in matches]
2256 return [leading + k + suf for k in matches]
1760
2257
2258 @context_matcher()
2259 def unicode_name_matcher(self, context: CompletionContext):
2260 """Same as :any:`unicode_name_matches`, but adopted to new Matcher API."""
2261 fragment, matches = self.unicode_name_matches(context.text_until_cursor)
2262 return _convert_matcher_v1_result_to_v2(
2263 matches, type="unicode", fragment=fragment, suppress_if_matches=True
2264 )
2265
1761 @staticmethod
2266 @staticmethod
1762 def unicode_name_matches(text:str) -> Tuple[str, List[str]] :
2267 def unicode_name_matches(text: str) -> Tuple[str, List[str]]:
1763 """Match Latex-like syntax for unicode characters base
2268 """Match Latex-like syntax for unicode characters base
1764 on the name of the character.
2269 on the name of the character.
1765
2270
@@ -1780,11 +2285,24 b' class IPCompleter(Completer):'
1780 pass
2285 pass
1781 return '', []
2286 return '', []
1782
2287
2288 @context_matcher()
2289 def latex_name_matcher(self, context: CompletionContext):
2290 """Match Latex syntax for unicode characters.
1783
2291
1784 def latex_matches(self, text:str) -> Tuple[str, Sequence[str]]:
2292 This does both ``\\alp`` -> ``\\alpha`` and ``\\alpha`` -> ``α``
2293 """
2294 fragment, matches = self.latex_matches(context.text_until_cursor)
2295 return _convert_matcher_v1_result_to_v2(
2296 matches, type="latex", fragment=fragment, suppress_if_matches=True
2297 )
2298
2299 def latex_matches(self, text: str) -> Tuple[str, Sequence[str]]:
1785 """Match Latex syntax for unicode characters.
2300 """Match Latex syntax for unicode characters.
1786
2301
1787 This does both ``\\alp`` -> ``\\alpha`` and ``\\alpha`` -> ``α``
2302 This does both ``\\alp`` -> ``\\alpha`` and ``\\alpha`` -> ``α``
2303
2304 .. deprecated:: 8.6
2305 You can use :meth:`latex_name_matcher` instead.
1788 """
2306 """
1789 slashpos = text.rfind('\\')
2307 slashpos = text.rfind('\\')
1790 if slashpos > -1:
2308 if slashpos > -1:
@@ -1801,7 +2319,25 b' class IPCompleter(Completer):'
1801 return s, matches
2319 return s, matches
1802 return '', ()
2320 return '', ()
1803
2321
2322 @context_matcher()
2323 def custom_completer_matcher(self, context):
2324 """Dispatch custom completer.
2325
2326 If a match is found, suppresses all other matchers except for Jedi.
2327 """
2328 matches = self.dispatch_custom_completer(context.token) or []
2329 result = _convert_matcher_v1_result_to_v2(
2330 matches, type=_UNKNOWN_TYPE, suppress_if_matches=True
2331 )
2332 result["ordered"] = True
2333 result["do_not_suppress"] = {_get_matcher_id(self._jedi_matcher)}
2334 return result
2335
1804 def dispatch_custom_completer(self, text):
2336 def dispatch_custom_completer(self, text):
2337 """
2338 .. deprecated:: 8.6
2339 You can use :meth:`custom_completer_matcher` instead.
2340 """
1805 if not self.custom_completers:
2341 if not self.custom_completers:
1806 return
2342 return
1807
2343
@@ -1955,12 +2491,25 b' class IPCompleter(Completer):'
1955 """
2491 """
1956 deadline = time.monotonic() + _timeout
2492 deadline = time.monotonic() + _timeout
1957
2493
1958
1959 before = full_text[:offset]
2494 before = full_text[:offset]
1960 cursor_line, cursor_column = position_to_cursor(full_text, offset)
2495 cursor_line, cursor_column = position_to_cursor(full_text, offset)
1961
2496
1962 matched_text, matches, matches_origin, jedi_matches = self._complete(
2497 jedi_matcher_id = _get_matcher_id(self._jedi_matcher)
1963 full_text=full_text, cursor_line=cursor_line, cursor_pos=cursor_column)
2498
2499 results = self._complete(
2500 full_text=full_text, cursor_line=cursor_line, cursor_pos=cursor_column
2501 )
2502 non_jedi_results: Dict[str, SimpleMatcherResult] = {
2503 identifier: result
2504 for identifier, result in results.items()
2505 if identifier != jedi_matcher_id
2506 }
2507
2508 jedi_matches = (
2509 cast(results[jedi_matcher_id], _JediMatcherResult)["completions"]
2510 if jedi_matcher_id in results
2511 else ()
2512 )
1964
2513
1965 iter_jm = iter(jedi_matches)
2514 iter_jm = iter(jedi_matches)
1966 if _timeout:
2515 if _timeout:
@@ -1988,28 +2537,57 b' class IPCompleter(Completer):'
1988
2537
1989 for jm in iter_jm:
2538 for jm in iter_jm:
1990 delta = len(jm.name_with_symbols) - len(jm.complete)
2539 delta = len(jm.name_with_symbols) - len(jm.complete)
1991 yield Completion(start=offset - delta,
2540 yield Completion(
1992 end=offset,
2541 start=offset - delta,
1993 text=jm.name_with_symbols,
2542 end=offset,
1994 type='<unknown>', # don't compute type for speed
2543 text=jm.name_with_symbols,
1995 _origin='jedi',
2544 type=_UNKNOWN_TYPE, # don't compute type for speed
1996 signature='')
2545 _origin="jedi",
1997
2546 signature="",
1998
2547 )
1999 start_offset = before.rfind(matched_text)
2000
2548
2001 # TODO:
2549 # TODO:
2002 # Suppress this, right now just for debug.
2550 # Suppress this, right now just for debug.
2003 if jedi_matches and matches and self.debug:
2551 if jedi_matches and non_jedi_results and self.debug:
2004 yield Completion(start=start_offset, end=offset, text='--jedi/ipython--',
2552 some_start_offset = before.rfind(
2005 _origin='debug', type='none', signature='')
2553 next(iter(non_jedi_results.values()))["matched_fragment"]
2554 )
2555 yield Completion(
2556 start=some_start_offset,
2557 end=offset,
2558 text="--jedi/ipython--",
2559 _origin="debug",
2560 type="none",
2561 signature="",
2562 )
2006
2563
2007 # I'm unsure if this is always true, so let's assert and see if it
2564 ordered = []
2008 # crash
2565 sortable = []
2009 assert before.endswith(matched_text)
2566
2010 for m, t in zip(matches, matches_origin):
2567 for origin, result in non_jedi_results.items():
2011 yield Completion(start=start_offset, end=offset, text=m, _origin=t, signature='', type='<unknown>')
2568 matched_text = result["matched_fragment"]
2569 start_offset = before.rfind(matched_text)
2570 is_ordered = result.get("ordered", False)
2571 container = ordered if is_ordered else sortable
2572
2573 # I'm unsure if this is always true, so let's assert and see if it
2574 # crash
2575 assert before.endswith(matched_text)
2576
2577 for simple_completion in result["completions"]:
2578 completion = Completion(
2579 start=start_offset,
2580 end=offset,
2581 text=simple_completion.text,
2582 _origin=origin,
2583 signature="",
2584 type=simple_completion.type or _UNKNOWN_TYPE,
2585 )
2586 container.append(completion)
2012
2587
2588 yield from list(self._deduplicate(ordered + self._sort(sortable)))[
2589 :MATCHES_LIMIT
2590 ]
2013
2591
2014 def complete(self, text=None, line_buffer=None, cursor_pos=None) -> Tuple[str, Sequence[str]]:
2592 def complete(self, text=None, line_buffer=None, cursor_pos=None) -> Tuple[str, Sequence[str]]:
2015 """Find completions for the given text and line context.
2593 """Find completions for the given text and line context.
@@ -2050,7 +2628,56 b' class IPCompleter(Completer):'
2050 PendingDeprecationWarning)
2628 PendingDeprecationWarning)
2051 # potential todo, FOLD the 3rd throw away argument of _complete
2629 # potential todo, FOLD the 3rd throw away argument of _complete
2052 # into the first 2 one.
2630 # into the first 2 one.
2053 return self._complete(line_buffer=line_buffer, cursor_pos=cursor_pos, text=text, cursor_line=0)[:2]
2631 # TODO: Q: does the above refer to jedi completions (i.e. 0-indexed?)
2632 # TODO: should we deprecate now, or does it stay?
2633
2634 results = self._complete(
2635 line_buffer=line_buffer, cursor_pos=cursor_pos, text=text, cursor_line=0
2636 )
2637
2638 jedi_matcher_id = _get_matcher_id(self._jedi_matcher)
2639
2640 return self._arrange_and_extract(
2641 results,
2642 # TODO: can we confirm that excluding Jedi here was a deliberate choice in previous version?
2643 skip_matchers={jedi_matcher_id},
2644 # this API does not support different start/end positions (fragments of token).
2645 abort_if_offset_changes=True,
2646 )
2647
2648 def _arrange_and_extract(
2649 self,
2650 results: Dict[str, MatcherResult],
2651 skip_matchers: Set[str],
2652 abort_if_offset_changes: bool,
2653 ):
2654
2655 sortable = []
2656 ordered = []
2657 most_recent_fragment = None
2658 for identifier, result in results.items():
2659 if identifier in skip_matchers:
2660 continue
2661 if not result["completions"]:
2662 continue
2663 if not most_recent_fragment:
2664 most_recent_fragment = result["matched_fragment"]
2665 if (
2666 abort_if_offset_changes
2667 and result["matched_fragment"] != most_recent_fragment
2668 ):
2669 break
2670 if result.get("ordered", False):
2671 ordered.extend(result["completions"])
2672 else:
2673 sortable.extend(result["completions"])
2674
2675 if not most_recent_fragment:
2676 most_recent_fragment = "" # to satisfy typechecker (and just in case)
2677
2678 return most_recent_fragment, [
2679 m.text for m in self._deduplicate(ordered + self._sort(sortable))
2680 ]
2054
2681
2055 def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None,
2682 def _complete(self, *, cursor_line, cursor_pos, line_buffer=None, text=None,
2056 full_text=None) -> _CompleteResult:
2683 full_text=None) -> _CompleteResult:
@@ -2085,14 +2712,10 b' class IPCompleter(Completer):'
2085
2712
2086 Returns
2713 Returns
2087 -------
2714 -------
2088 A tuple of N elements which are (likely):
2715 An ordered dictionary where keys are identifiers of completion
2089 matched_text: ? the text that the complete matched
2716 matchers and values are ``MatcherResult``s.
2090 matches: list of completions ?
2091 matches_origin: ? list same length as matches, and where each completion came from
2092 jedi_matches: list of Jedi matches, have it's own structure.
2093 """
2717 """
2094
2718
2095
2096 # if the cursor position isn't given, the only sane assumption we can
2719 # if the cursor position isn't given, the only sane assumption we can
2097 # make is that it's at the end of the line (the common case)
2720 # make is that it's at the end of the line (the common case)
2098 if cursor_pos is None:
2721 if cursor_pos is None:
@@ -2104,98 +2727,156 b' class IPCompleter(Completer):'
2104 # if text is either None or an empty string, rely on the line buffer
2727 # if text is either None or an empty string, rely on the line buffer
2105 if (not line_buffer) and full_text:
2728 if (not line_buffer) and full_text:
2106 line_buffer = full_text.split('\n')[cursor_line]
2729 line_buffer = full_text.split('\n')[cursor_line]
2107 if not text: # issue #11508: check line_buffer before calling split_line
2730 if not text: # issue #11508: check line_buffer before calling split_line
2108 text = self.splitter.split_line(line_buffer, cursor_pos) if line_buffer else ''
2731 text = (
2109
2732 self.splitter.split_line(line_buffer, cursor_pos) if line_buffer else ""
2110 if self.backslash_combining_completions:
2733 )
2111 # allow deactivation of these on windows.
2112 base_text = text if not line_buffer else line_buffer[:cursor_pos]
2113
2114 for meth in (self.latex_matches,
2115 self.unicode_name_matches,
2116 back_latex_name_matches,
2117 back_unicode_name_matches,
2118 self.fwd_unicode_match):
2119 name_text, name_matches = meth(base_text)
2120 if name_text:
2121 return _CompleteResult(name_text, name_matches[:MATCHES_LIMIT], \
2122 [meth.__qualname__]*min(len(name_matches), MATCHES_LIMIT), ())
2123
2124
2734
2125 # If no line buffer is given, assume the input text is all there was
2735 # If no line buffer is given, assume the input text is all there was
2126 if line_buffer is None:
2736 if line_buffer is None:
2127 line_buffer = text
2737 line_buffer = text
2128
2738
2739 # deprecated - do not use `line_buffer` in new code.
2129 self.line_buffer = line_buffer
2740 self.line_buffer = line_buffer
2130 self.text_until_cursor = self.line_buffer[:cursor_pos]
2741 self.text_until_cursor = self.line_buffer[:cursor_pos]
2131
2742
2132 # Do magic arg matches
2743 if not full_text:
2133 for matcher in self.magic_arg_matchers:
2744 full_text = line_buffer
2134 matches = list(matcher(line_buffer))[:MATCHES_LIMIT]
2745
2135 if matches:
2746 context = CompletionContext(
2136 origins = [matcher.__qualname__] * len(matches)
2747 full_text=full_text,
2137 return _CompleteResult(text, matches, origins, ())
2748 cursor_position=cursor_pos,
2749 cursor_line=cursor_line,
2750 token=text,
2751 limit=MATCHES_LIMIT,
2752 )
2138
2753
2139 # Start with a clean slate of completions
2754 # Start with a clean slate of completions
2140 matches = []
2755 results = {}
2141
2756
2142 # FIXME: we should extend our api to return a dict with completions for
2757 jedi_matcher_id = _get_matcher_id(self._jedi_matcher)
2143 # different types of objects. The rlcomplete() method could then
2144 # simply collapse the dict into a list for readline, but we'd have
2145 # richer completion semantics in other environments.
2146 is_magic_prefix = len(text) > 0 and text[0] == "%"
2147 completions: Iterable[Any] = []
2148 if self.use_jedi and not is_magic_prefix:
2149 if not full_text:
2150 full_text = line_buffer
2151 completions = self._jedi_matches(
2152 cursor_pos, cursor_line, full_text)
2153
2154 if self.merge_completions:
2155 matches = []
2156 for matcher in self.matchers:
2157 try:
2158 matches.extend([(m, matcher.__qualname__)
2159 for m in matcher(text)])
2160 except:
2161 # Show the ugly traceback if the matcher causes an
2162 # exception, but do NOT crash the kernel!
2163 sys.excepthook(*sys.exc_info())
2164 else:
2165 for matcher in self.matchers:
2166 matches = [(m, matcher.__qualname__)
2167 for m in matcher(text)]
2168 if matches:
2169 break
2170
2171 seen = set()
2172 filtered_matches = set()
2173 for m in matches:
2174 t, c = m
2175 if t not in seen:
2176 filtered_matches.add(m)
2177 seen.add(t)
2178
2758
2179 _filtered_matches = sorted(filtered_matches, key=lambda x: completions_sorting_key(x[0]))
2759 suppressed_matchers = set()
2180
2760
2181 custom_res = [(m, 'custom') for m in self.dispatch_custom_completer(text) or []]
2761 matchers = {
2182
2762 _get_matcher_id(matcher): matcher
2183 _filtered_matches = custom_res or _filtered_matches
2763 for matcher in sorted(
2184
2764 self.matchers, key=_get_matcher_priority, reverse=True
2185 _filtered_matches = _filtered_matches[:MATCHES_LIMIT]
2765 )
2186 _matches = [m[0] for m in _filtered_matches]
2766 }
2187 origins = [m[1] for m in _filtered_matches]
2188
2767
2189 self.matches = _matches
2768 for matcher_id, matcher in matchers.items():
2769 api_version = _get_matcher_api_version(matcher)
2770 matcher_id = _get_matcher_id(matcher)
2190
2771
2191 return _CompleteResult(text, _matches, origins, completions)
2772 if matcher_id in self.disable_matchers:
2192
2773 continue
2193 def fwd_unicode_match(self, text:str) -> Tuple[str, Sequence[str]]:
2774
2775 if matcher_id in results:
2776 warnings.warn(f"Duplicate matcher ID: {matcher_id}.")
2777
2778 if matcher_id in suppressed_matchers:
2779 continue
2780
2781 try:
2782 if api_version == 1:
2783 result = _convert_matcher_v1_result_to_v2(
2784 matcher(text), type=_UNKNOWN_TYPE
2785 )
2786 elif api_version == 2:
2787 result = cast(matcher, MatcherAPIv2)(context)
2788 else:
2789 raise ValueError(f"Unsupported API version {api_version}")
2790 except:
2791 # Show the ugly traceback if the matcher causes an
2792 # exception, but do NOT crash the kernel!
2793 sys.excepthook(*sys.exc_info())
2794 continue
2795
2796 # set default value for matched fragment if suffix was not selected.
2797 result["matched_fragment"] = result.get("matched_fragment", context.token)
2798
2799 if not suppressed_matchers:
2800 suppression_recommended = result.get("suppress", False)
2801
2802 suppression_config = (
2803 self.suppress_competing_matchers.get(matcher_id, None)
2804 if isinstance(self.suppress_competing_matchers, dict)
2805 else self.suppress_competing_matchers
2806 )
2807 should_suppress = (
2808 (suppression_config is True)
2809 or (suppression_recommended and (suppression_config is not False))
2810 ) and len(result["completions"])
2811
2812 if should_suppress:
2813 suppression_exceptions = result.get("do_not_suppress", set())
2814 try:
2815 to_suppress = set(suppression_recommended)
2816 except TypeError:
2817 to_suppress = set(matchers)
2818 suppressed_matchers = to_suppress - suppression_exceptions
2819
2820 new_results = {}
2821 for previous_matcher_id, previous_result in results.items():
2822 if previous_matcher_id not in suppressed_matchers:
2823 new_results[previous_matcher_id] = previous_result
2824 results = new_results
2825
2826 results[matcher_id] = result
2827
2828 _, matches = self._arrange_and_extract(
2829 results,
2830 # TODO Jedi completions non included in legacy stateful API; was this deliberate or omission?
2831 # if it was omission, we can remove the filtering step, otherwise remove this comment.
2832 skip_matchers={jedi_matcher_id},
2833 abort_if_offset_changes=False,
2834 )
2835
2836 # populate legacy stateful API
2837 self.matches = matches
2838
2839 return results
2840
2841 @staticmethod
2842 def _deduplicate(
2843 matches: Sequence[SimpleCompletion],
2844 ) -> Iterable[SimpleCompletion]:
2845 filtered_matches = {}
2846 for match in matches:
2847 text = match.text
2848 if (
2849 text not in filtered_matches
2850 or filtered_matches[text].type == _UNKNOWN_TYPE
2851 ):
2852 filtered_matches[text] = match
2853
2854 return filtered_matches.values()
2855
2856 @staticmethod
2857 def _sort(matches: Sequence[SimpleCompletion]):
2858 return sorted(matches, key=lambda x: completions_sorting_key(x.text))
2859
2860 @context_matcher()
2861 def fwd_unicode_matcher(self, context: CompletionContext):
2862 """Same as :any:`fwd_unicode_match`, but adopted to new Matcher API."""
2863 # TODO: use `context.limit` to terminate early once we matched the maximum
2864 # number that will be used downstream; can be added as an optional to
2865 # `fwd_unicode_match(text: str, limit: int = None)` or we could re-implement here.
2866 fragment, matches = self.fwd_unicode_match(context.text_until_cursor)
2867 return _convert_matcher_v1_result_to_v2(
2868 matches, type="unicode", fragment=fragment, suppress_if_matches=True
2869 )
2870
2871 def fwd_unicode_match(self, text: str) -> Tuple[str, Sequence[str]]:
2194 """
2872 """
2195 Forward match a string starting with a backslash with a list of
2873 Forward match a string starting with a backslash with a list of
2196 potential Unicode completions.
2874 potential Unicode completions.
2197
2875
2198 Will compute list list of Unicode character names on first call and cache it.
2876 Will compute list of Unicode character names on first call and cache it.
2877
2878 .. deprecated:: 8.6
2879 You can use :meth:`fwd_unicode_matcher` instead.
2199
2880
2200 Returns
2881 Returns
2201 -------
2882 -------
@@ -80,6 +80,9 b' class ConfigMagics(Magics):'
80 Enable debug for the Completer. Mostly print extra information for
80 Enable debug for the Completer. Mostly print extra information for
81 experimental jedi integration.
81 experimental jedi integration.
82 Current: False
82 Current: False
83 IPCompleter.disable_matchers=<list-item-1>...
84 List of matchers to disable.
85 Current: []
83 IPCompleter.greedy=<Bool>
86 IPCompleter.greedy=<Bool>
84 Activate greedy completion
87 Activate greedy completion
85 PENDING DEPRECATION. this is now mostly taken care of with Jedi.
88 PENDING DEPRECATION. this is now mostly taken care of with Jedi.
@@ -102,6 +105,8 b' class ConfigMagics(Magics):'
102 Whether to merge completion results into a single list
105 Whether to merge completion results into a single list
103 If False, only the completion results from the first non-empty
106 If False, only the completion results from the first non-empty
104 completer will be returned.
107 completer will be returned.
108 As of version 8.6.0, setting the value to ``False`` is an alias for:
109 ``IPCompleter.suppress_competing_matchers = True.``.
105 Current: True
110 Current: True
106 IPCompleter.omit__names=<Enum>
111 IPCompleter.omit__names=<Enum>
107 Instruct the completer to omit private method names
112 Instruct the completer to omit private method names
@@ -117,6 +122,24 b' class ConfigMagics(Magics):'
117 IPCompleter.profiler_output_dir=<Unicode>
122 IPCompleter.profiler_output_dir=<Unicode>
118 Template for path at which to output profile data for completions.
123 Template for path at which to output profile data for completions.
119 Current: '.completion_profiles'
124 Current: '.completion_profiles'
125 IPCompleter.suppress_competing_matchers=<Union>
126 Whether to suppress completions from other *Matchers*.
127 When set to ``None`` (default) the matchers will attempt to auto-detect
128 whether suppression of other matchers is desirable. For example, at the
129 beginning of a line followed by `%` we expect a magic completion to be the
130 only applicable option, and after ``my_dict['`` we usually expect a
131 completion with an existing dictionary key.
132 If you want to disable this heuristic and see completions from all matchers,
133 set ``IPCompleter.suppress_competing_matchers = False``. To disable the
134 heuristic for specific matchers provide a dictionary mapping:
135 ``IPCompleter.suppress_competing_matchers = {'IPCompleter.dict_key_matcher':
136 False}``.
137 Set ``IPCompleter.suppress_competing_matchers = True`` to limit completions
138 to the set of matchers with the highest priority; this is equivalent to
139 ``IPCompleter.merge_completions`` and can be beneficial for performance, but
140 will sometimes omit relevant candidates from matchers further down the
141 priority list.
142 Current: None
120 IPCompleter.use_jedi=<Bool>
143 IPCompleter.use_jedi=<Bool>
121 Experimental: Use Jedi to generate autocompletions. Default to True if jedi
144 Experimental: Use Jedi to generate autocompletions. Default to True if jedi
122 is installed.
145 is installed.
@@ -24,6 +24,9 b' from IPython.core.completer import ('
24 provisionalcompleter,
24 provisionalcompleter,
25 match_dict_keys,
25 match_dict_keys,
26 _deduplicate_completions,
26 _deduplicate_completions,
27 completion_matcher,
28 SimpleCompletion,
29 CompletionContext,
27 )
30 )
28
31
29 # -----------------------------------------------------------------------------
32 # -----------------------------------------------------------------------------
@@ -109,6 +112,16 b' def greedy_completion():'
109 ip.Completer.greedy = greedy_original
112 ip.Completer.greedy = greedy_original
110
113
111
114
115 @contextmanager
116 def custom_matchers(matchers):
117 ip = get_ipython()
118 try:
119 ip.Completer.custom_matchers.extend(matchers)
120 yield
121 finally:
122 ip.Completer.custom_matchers.clear()
123
124
112 def test_protect_filename():
125 def test_protect_filename():
113 if sys.platform == "win32":
126 if sys.platform == "win32":
114 pairs = [
127 pairs = [
@@ -298,7 +311,7 b' class TestCompleter(unittest.TestCase):'
298 ip = get_ipython()
311 ip = get_ipython()
299
312
300 name, matches = ip.complete("\\Ⅴ")
313 name, matches = ip.complete("\\Ⅴ")
301 self.assertEqual(matches, ("\\ROMAN NUMERAL FIVE",))
314 self.assertEqual(matches, ["\\ROMAN NUMERAL FIVE"])
302
315
303 def test_forward_unicode_completion(self):
316 def test_forward_unicode_completion(self):
304 ip = get_ipython()
317 ip = get_ipython()
@@ -379,6 +392,12 b' class TestCompleter(unittest.TestCase):'
379
392
380 def test_quoted_file_completions(self):
393 def test_quoted_file_completions(self):
381 ip = get_ipython()
394 ip = get_ipython()
395
396 def _(text):
397 return ip.Completer._complete(
398 cursor_line=0, cursor_pos=len(text), full_text=text
399 )["IPCompleter.file_matcher"]["completions"]
400
382 with TemporaryWorkingDirectory():
401 with TemporaryWorkingDirectory():
383 name = "foo'bar"
402 name = "foo'bar"
384 open(name, "w", encoding="utf-8").close()
403 open(name, "w", encoding="utf-8").close()
@@ -387,25 +406,16 b' class TestCompleter(unittest.TestCase):'
387 escaped = name if sys.platform == "win32" else "foo\\'bar"
406 escaped = name if sys.platform == "win32" else "foo\\'bar"
388
407
389 # Single quote matches embedded single quote
408 # Single quote matches embedded single quote
390 text = "open('foo"
409 c = _("open('foo")[0]
391 c = ip.Completer._complete(
410 self.assertEqual(c.text, escaped)
392 cursor_line=0, cursor_pos=len(text), full_text=text
393 )[1]
394 self.assertEqual(c, [escaped])
395
411
396 # Double quote requires no escape
412 # Double quote requires no escape
397 text = 'open("foo'
413 c = _('open("foo')[0]
398 c = ip.Completer._complete(
414 self.assertEqual(c.text, name)
399 cursor_line=0, cursor_pos=len(text), full_text=text
400 )[1]
401 self.assertEqual(c, [name])
402
415
403 # No quote requires an escape
416 # No quote requires an escape
404 text = "%ls foo"
417 c = _("%ls foo")[0]
405 c = ip.Completer._complete(
418 self.assertEqual(c.text, escaped)
406 cursor_line=0, cursor_pos=len(text), full_text=text
407 )[1]
408 self.assertEqual(c, [escaped])
409
419
410 def test_all_completions_dups(self):
420 def test_all_completions_dups(self):
411 """
421 """
@@ -475,6 +485,17 b' class TestCompleter(unittest.TestCase):'
475 "encoding" in c.signature
485 "encoding" in c.signature
476 ), "Signature of function was not found by completer"
486 ), "Signature of function was not found by completer"
477
487
488 def test_completions_have_type(self):
489 """
490 Lets make sure matchers provide completion type.
491 """
492 ip = get_ipython()
493 with provisionalcompleter():
494 ip.Completer.use_jedi = False
495 completions = ip.Completer.completions("%tim", 3)
496 c = next(completions) # should be `%time` or similar
497 assert c.type == "magic", "Type of magic was not assigned by completer"
498
478 @pytest.mark.xfail(reason="Known failure on jedi<=0.18.0")
499 @pytest.mark.xfail(reason="Known failure on jedi<=0.18.0")
479 def test_deduplicate_completions(self):
500 def test_deduplicate_completions(self):
480 """
501 """
@@ -1273,3 +1294,153 b' class TestCompleter(unittest.TestCase):'
1273 completions = completer.completions(text, len(text))
1294 completions = completer.completions(text, len(text))
1274 for c in completions:
1295 for c in completions:
1275 self.assertEqual(c.text[0], "%")
1296 self.assertEqual(c.text[0], "%")
1297
1298 def test_fwd_unicode_restricts(self):
1299 ip = get_ipython()
1300 completer = ip.Completer
1301 text = "\\ROMAN NUMERAL FIVE"
1302
1303 with provisionalcompleter():
1304 completer.use_jedi = True
1305 completions = [
1306 completion.text for completion in completer.completions(text, len(text))
1307 ]
1308 self.assertEqual(completions, ["\u2164"])
1309
1310 def test_dict_key_restrict_to_dicts(self):
1311 """Test that dict key suppresses non-dict completion items"""
1312 ip = get_ipython()
1313 c = ip.Completer
1314 d = {"abc": None}
1315 ip.user_ns["d"] = d
1316
1317 text = 'd["a'
1318
1319 def _():
1320 with provisionalcompleter():
1321 c.use_jedi = True
1322 return [
1323 completion.text for completion in c.completions(text, len(text))
1324 ]
1325
1326 completions = _()
1327 self.assertEqual(completions, ["abc"])
1328
1329 # check that it can be disabled in granular manner:
1330 cfg = Config()
1331 cfg.IPCompleter.suppress_competing_matchers = {
1332 "IPCompleter.dict_key_matcher": False
1333 }
1334 c.update_config(cfg)
1335
1336 completions = _()
1337 self.assertIn("abc", completions)
1338 self.assertGreater(len(completions), 1)
1339
1340 def test_matcher_suppression(self):
1341 @completion_matcher(identifier="a_matcher")
1342 def a_matcher(text):
1343 return ["completion_a"]
1344
1345 @completion_matcher(identifier="b_matcher", api_version=2)
1346 def b_matcher(context: CompletionContext):
1347 text = context.token
1348 result = {"completions": [SimpleCompletion("completion_b")]}
1349
1350 if text == "suppress c":
1351 result["suppress"] = {"c_matcher"}
1352
1353 if text.startswith("suppress all"):
1354 result["suppress"] = True
1355 if text == "suppress all but c":
1356 result["do_not_suppress"] = {"c_matcher"}
1357 if text == "suppress all but a":
1358 result["do_not_suppress"] = {"a_matcher"}
1359
1360 return result
1361
1362 @completion_matcher(identifier="c_matcher")
1363 def c_matcher(text):
1364 return ["completion_c"]
1365
1366 with custom_matchers([a_matcher, b_matcher, c_matcher]):
1367 ip = get_ipython()
1368 c = ip.Completer
1369
1370 def _(text, expected):
1371 c.use_jedi = False
1372 s, matches = c.complete(text)
1373 self.assertEqual(expected, matches)
1374
1375 _("do not suppress", ["completion_a", "completion_b", "completion_c"])
1376 _("suppress all", ["completion_b"])
1377 _("suppress all but a", ["completion_a", "completion_b"])
1378 _("suppress all but c", ["completion_b", "completion_c"])
1379
1380 def configure(suppression_config):
1381 cfg = Config()
1382 cfg.IPCompleter.suppress_competing_matchers = suppression_config
1383 c.update_config(cfg)
1384
1385 # test that configuration takes priority over the run-time decisions
1386
1387 configure(False)
1388 _("suppress all", ["completion_a", "completion_b", "completion_c"])
1389
1390 configure({"b_matcher": False})
1391 _("suppress all", ["completion_a", "completion_b", "completion_c"])
1392
1393 configure({"a_matcher": False})
1394 _("suppress all", ["completion_b"])
1395
1396 configure({"b_matcher": True})
1397 _("do not suppress", ["completion_b"])
1398
1399 def test_matcher_disabling(self):
1400 @completion_matcher(identifier="a_matcher")
1401 def a_matcher(text):
1402 return ["completion_a"]
1403
1404 @completion_matcher(identifier="b_matcher")
1405 def b_matcher(text):
1406 return ["completion_b"]
1407
1408 def _(expected):
1409 s, matches = c.complete("completion_")
1410 self.assertEqual(expected, matches)
1411
1412 with custom_matchers([a_matcher, b_matcher]):
1413 ip = get_ipython()
1414 c = ip.Completer
1415
1416 _(["completion_a", "completion_b"])
1417
1418 cfg = Config()
1419 cfg.IPCompleter.disable_matchers = ["b_matcher"]
1420 c.update_config(cfg)
1421
1422 _(["completion_a"])
1423
1424 cfg.IPCompleter.disable_matchers = []
1425 c.update_config(cfg)
1426
1427 def test_matcher_priority(self):
1428 @completion_matcher(identifier="a_matcher", priority=0, api_version=2)
1429 def a_matcher(text):
1430 return {"completions": [SimpleCompletion("completion_a")], "suppress": True}
1431
1432 @completion_matcher(identifier="b_matcher", priority=2, api_version=2)
1433 def b_matcher(text):
1434 return {"completions": [SimpleCompletion("completion_b")], "suppress": True}
1435
1436 def _(expected):
1437 s, matches = c.complete("completion_")
1438 self.assertEqual(expected, matches)
1439
1440 with custom_matchers([a_matcher, b_matcher]):
1441 ip = get_ipython()
1442 c = ip.Completer
1443
1444 _(["completion_b"])
1445 a_matcher.matcher_priority = 3
1446 _(["completion_a"])
@@ -2,7 +2,7 b''
2 """Decorators that don't go anywhere else.
2 """Decorators that don't go anywhere else.
3
3
4 This module contains misc. decorators that don't really go with another module
4 This module contains misc. decorators that don't really go with another module
5 in :mod:`IPython.utils`. Beore putting something here please see if it should
5 in :mod:`IPython.utils`. Before putting something here please see if it should
6 go into another topical module in :mod:`IPython.utils`.
6 go into another topical module in :mod:`IPython.utils`.
7 """
7 """
8
8
@@ -16,6 +16,10 b' go into another topical module in :mod:`IPython.utils`.'
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 from typing import Sequence
20
21 from IPython.utils.docs import GENERATING_DOCUMENTATION
22
19
23
20 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
21 # Code
25 # Code
@@ -48,6 +52,7 b' def flag_calls(func):'
48 wrapper.__doc__ = func.__doc__
52 wrapper.__doc__ = func.__doc__
49 return wrapper
53 return wrapper
50
54
55
51 def undoc(func):
56 def undoc(func):
52 """Mark a function or class as undocumented.
57 """Mark a function or class as undocumented.
53
58
@@ -56,3 +61,23 b' def undoc(func):'
56 """
61 """
57 return func
62 return func
58
63
64
65 def sphinx_options(
66 show_inheritance: bool = True,
67 show_inherited_members: bool = False,
68 exclude_inherited_from: Sequence[str] = tuple(),
69 ):
70 """Set sphinx options"""
71
72 def wrapper(func):
73 if not GENERATING_DOCUMENTATION:
74 return func
75
76 func._sphinx_options = dict(
77 show_inheritance=show_inheritance,
78 show_inherited_members=show_inherited_members,
79 exclude_inherited_from=exclude_inherited_from,
80 )
81 return func
82
83 return wrapper
@@ -41,6 +41,14 b' else:'
41 html_theme = "sphinx_rtd_theme"
41 html_theme = "sphinx_rtd_theme"
42 html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
42 html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
43
43
44 # Allow Python scripts to change behaviour during sphinx run
45 os.environ["IN_SPHINX_RUN"] = "True"
46
47 autodoc_type_aliases = {
48 "Matcher": " IPython.core.completer.Matcher",
49 "MatcherAPIv1": " IPython.core.completer.MatcherAPIv1",
50 }
51
44 # If your extensions are in another directory, add it here. If the directory
52 # If your extensions are in another directory, add it here. If the directory
45 # is relative to the documentation root, use os.path.abspath to make it
53 # is relative to the documentation root, use os.path.abspath to make it
46 # absolute, like shown here.
54 # absolute, like shown here.
@@ -24,14 +24,9 b' import inspect'
24 import os
24 import os
25 import re
25 import re
26 from importlib import import_module
26 from importlib import import_module
27 from types import SimpleNamespace as Obj
27
28
28
29
29 class Obj(object):
30 '''Namespace to hold arbitrary information.'''
31 def __init__(self, **kwargs):
32 for k, v in kwargs.items():
33 setattr(self, k, v)
34
35 class FuncClsScanner(ast.NodeVisitor):
30 class FuncClsScanner(ast.NodeVisitor):
36 """Scan a module for top-level functions and classes.
31 """Scan a module for top-level functions and classes.
37
32
@@ -42,7 +37,7 b' class FuncClsScanner(ast.NodeVisitor):'
42 self.classes = []
37 self.classes = []
43 self.classes_seen = set()
38 self.classes_seen = set()
44 self.functions = []
39 self.functions = []
45
40
46 @staticmethod
41 @staticmethod
47 def has_undoc_decorator(node):
42 def has_undoc_decorator(node):
48 return any(isinstance(d, ast.Name) and d.id == 'undoc' \
43 return any(isinstance(d, ast.Name) and d.id == 'undoc' \
@@ -62,11 +57,15 b' class FuncClsScanner(ast.NodeVisitor):'
62 self.functions.append(node.name)
57 self.functions.append(node.name)
63
58
64 def visit_ClassDef(self, node):
59 def visit_ClassDef(self, node):
65 if not (node.name.startswith('_') or self.has_undoc_decorator(node)) \
60 if (
66 and node.name not in self.classes_seen:
61 not (node.name.startswith("_") or self.has_undoc_decorator(node))
67 cls = Obj(name=node.name)
62 and node.name not in self.classes_seen
68 cls.has_init = any(isinstance(n, ast.FunctionDef) and \
63 ):
69 n.name=='__init__' for n in node.body)
64 cls = Obj(name=node.name, sphinx_options={})
65 cls.has_init = any(
66 isinstance(n, ast.FunctionDef) and n.name == "__init__"
67 for n in node.body
68 )
70 self.classes.append(cls)
69 self.classes.append(cls)
71 self.classes_seen.add(node.name)
70 self.classes_seen.add(node.name)
72
71
@@ -221,7 +220,11 b' class ApiDocWriter(object):'
221 funcs, classes = [], []
220 funcs, classes = [], []
222 for name, obj in ns.items():
221 for name, obj in ns.items():
223 if inspect.isclass(obj):
222 if inspect.isclass(obj):
224 cls = Obj(name=name, has_init='__init__' in obj.__dict__)
223 cls = Obj(
224 name=name,
225 has_init="__init__" in obj.__dict__,
226 sphinx_options=getattr(obj, "_sphinx_options", {}),
227 )
225 classes.append(cls)
228 classes.append(cls)
226 elif inspect.isfunction(obj):
229 elif inspect.isfunction(obj):
227 funcs.append(name)
230 funcs.append(name)
@@ -279,10 +282,18 b' class ApiDocWriter(object):'
279 self.rst_section_levels[2] * len(subhead) + '\n'
282 self.rst_section_levels[2] * len(subhead) + '\n'
280
283
281 for c in classes:
284 for c in classes:
282 ad += '\n.. autoclass:: ' + c.name + '\n'
285 opts = c.sphinx_options
286 ad += "\n.. autoclass:: " + c.name + "\n"
283 # must NOT exclude from index to keep cross-refs working
287 # must NOT exclude from index to keep cross-refs working
284 ad += ' :members:\n' \
288 ad += " :members:\n"
285 ' :show-inheritance:\n'
289 if opts.get("show_inheritance", True):
290 ad += " :show-inheritance:\n"
291 if opts.get("show_inherited_members", False):
292 exclusions_list = opts.get("exclude_inherited_from", [])
293 exclusions = (
294 (" " + " ".join(exclusions_list)) if exclusions_list else ""
295 )
296 ad += f" :inherited-members:{exclusions}\n"
286 if c.has_init:
297 if c.has_init:
287 ad += '\n .. automethod:: __init__\n'
298 ad += '\n .. automethod:: __init__\n'
288
299
@@ -54,6 +54,7 b' doc ='
54 matplotlib
54 matplotlib
55 stack_data
55 stack_data
56 pytest<7
56 pytest<7
57 typing_extensions
57 %(test)s
58 %(test)s
58 kernel =
59 kernel =
59 ipykernel
60 ipykernel
General Comments 0
You need to be logged in to leave comments. Login now