@@ -898,7 +898,7 @@ def rectify_completions(text: str, completions: _IC, *, _debug: bool = False) ->
898898 new_text = text [new_start :c .start ] + c .text + text [c .end :new_end ]
899899 if c ._origin == 'jedi' :
900900 seen_jedi .add (new_text )
901- elif c ._origin == ' IPCompleter.python_matches' :
901+ elif c ._origin == " IPCompleter.python_matcher" :
902902 seen_python_matches .add (new_text )
903903 yield Completion (new_start , new_end , new_text , type = c .type , _origin = c ._origin , signature = c .signature )
904904 diff = seen_python_matches .difference (seen_jedi )
@@ -1139,15 +1139,18 @@ def attr_matches(self, text):
11391139 with a __getattr__ hook is evaluated.
11401140
11411141 """
1142+ return self ._attr_matches (text )[0 ]
1143+
1144+ def _attr_matches (self , text , include_prefix = True ) -> Tuple [Sequence [str ], str ]:
11421145 m2 = re .match (r"(.+)\.(\w*)$" , self .line_buffer )
11431146 if not m2 :
1144- return []
1147+ return [], ""
11451148 expr , attr = m2 .group (1 , 2 )
11461149
11471150 obj = self ._evaluate_expr (expr )
11481151
11491152 if obj is not_found :
1150- return []
1153+ return [], ""
11511154
11521155 if self .limit_to__all__ and hasattr (obj , '__all__' ):
11531156 words = get__all__entries (obj )
@@ -1170,28 +1173,36 @@ def attr_matches(self, text):
11701173 # reconciliator would know that we intend to append to rather than
11711174 # replace the input text; this requires refactoring to return range
11721175 # which ought to be replaced (as does jedi).
1173- tokens = _parse_tokens (expr )
1174- rev_tokens = reversed (tokens )
1175- skip_over = {tokenize .ENDMARKER , tokenize .NEWLINE }
1176- name_turn = True
1177-
1178- parts = []
1179- for token in rev_tokens :
1180- if token .type in skip_over :
1181- continue
1182- if token .type == tokenize .NAME and name_turn :
1183- parts .append (token .string )
1184- name_turn = False
1185- elif token .type == tokenize .OP and token .string == "." and not name_turn :
1186- parts .append (token .string )
1187- name_turn = True
1188- else :
1189- # short-circuit if not empty nor name token
1190- break
1176+ if include_prefix :
1177+ tokens = _parse_tokens (expr )
1178+ rev_tokens = reversed (tokens )
1179+ skip_over = {tokenize .ENDMARKER , tokenize .NEWLINE }
1180+ name_turn = True
1181+
1182+ parts = []
1183+ for token in rev_tokens :
1184+ if token .type in skip_over :
1185+ continue
1186+ if token .type == tokenize .NAME and name_turn :
1187+ parts .append (token .string )
1188+ name_turn = False
1189+ elif (
1190+ token .type == tokenize .OP and token .string == "." and not name_turn
1191+ ):
1192+ parts .append (token .string )
1193+ name_turn = True
1194+ else :
1195+ # short-circuit if not empty nor name token
1196+ break
11911197
1192- prefix_after_space = "" .join (reversed (parts ))
1198+ prefix_after_space = "" .join (reversed (parts ))
1199+ else :
1200+ prefix_after_space = ""
11931201
1194- return ["%s.%s" % (prefix_after_space , w ) for w in words if w [:n ] == attr ]
1202+ return (
1203+ ["%s.%s" % (prefix_after_space , w ) for w in words if w [:n ] == attr ],
1204+ "." + attr ,
1205+ )
11951206
11961207 def _evaluate_expr (self , expr ):
11971208 obj = not_found
@@ -1973,9 +1984,8 @@ def matchers(self) -> List[Matcher]:
19731984 * self .magic_arg_matchers ,
19741985 self .custom_completer_matcher ,
19751986 self .dict_key_matcher ,
1976- # TODO: convert python_matches to v2 API
19771987 self .magic_matcher ,
1978- self .python_matches ,
1988+ self .python_matcher ,
19791989 self .file_matcher ,
19801990 self .python_func_kw_matcher ,
19811991 ]
@@ -2316,9 +2326,42 @@ def _jedi_matches(
23162326 else :
23172327 return iter ([])
23182328
2329+ @context_matcher ()
2330+ def python_matcher (self , context : CompletionContext ) -> SimpleMatcherResult :
2331+ """Match attributes or global python names"""
2332+ text = context .line_with_cursor
2333+ if "." in text :
2334+ try :
2335+ matches , fragment = self ._attr_matches (text , include_prefix = False )
2336+ if text .endswith ("." ) and self .omit__names :
2337+ if self .omit__names == 1 :
2338+ # true if txt is _not_ a __ name, false otherwise:
2339+ no__name = lambda txt : re .match (r".*\.__.*?__" , txt ) is None
2340+ else :
2341+ # true if txt is _not_ a _ name, false otherwise:
2342+ no__name = (
2343+ lambda txt : re .match (r"\._.*?" , txt [txt .rindex ("." ) :])
2344+ is None
2345+ )
2346+ matches = filter (no__name , matches )
2347+ return _convert_matcher_v1_result_to_v2 (
2348+ matches , type = "attribute" , fragment = fragment
2349+ )
2350+ except NameError :
2351+ # catches <undefined attributes>.<tab>
2352+ matches = []
2353+ return _convert_matcher_v1_result_to_v2 (matches , type = "attribute" )
2354+ else :
2355+ matches = self .global_matches (context .token )
2356+ # TODO: maybe distinguish between functions, modules and just "variables"
2357+ return _convert_matcher_v1_result_to_v2 (matches , type = "variable" )
2358+
23192359 @completion_matcher (api_version = 1 )
23202360 def python_matches (self , text : str ) -> Iterable [str ]:
2321- """Match attributes or global python names"""
2361+ """Match attributes or global python names.
2362+
2363+ .. deprecated:: 8.27
2364+ You can use :meth:`python_matcher` instead."""
23222365 if "." in text :
23232366 try :
23242367 matches = self .attr_matches (text )
0 commit comments