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__ = [ |
|
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( |
|
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( |
|
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_matche |
|
1567 | self.magic_config_matcher, | |
1181 |
self.magic_color_matche |
|
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[ |
|
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_matche |
|
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. |
|
1604 | *self._backslash_combining_matchers, | |
1203 |
self. |
|
1605 | *self.magic_arg_matchers, | |
1204 |
self. |
|
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. |
|
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_matche |
|
1622 | self.file_matcher, | |
1212 |
self. |
|
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.Completion |
|
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( |
|
2540 | yield Completion( | |
1992 |
|
|
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 |
|
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 |
|
|
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 |
|
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, |
|
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 |
|
|
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 |
|
|
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 |
|
|
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 += |
|
288 | ad += " :members:\n" | |
285 |
|
|
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 |
General Comments 0
You need to be logged in to leave comments.
Login now