##// END OF EJS Templates
Add dict key completion in IPCompleter
Joel Nothman -
Show More
@@ -426,6 +426,50 b' def get__all__entries(obj):'
426 return [w for w in words if isinstance(w, string_types)]
426 return [w for w in words if isinstance(w, string_types)]
427
427
428
428
429 def match_dict_keys(keys, prefix):
430 """Used by dict_key_matches, matching the prefix to a list of keys"""
431 if not prefix:
432 return None, [repr(k) for k in keys
433 if isinstance(k, (string_types, bytes))]
434 quote_match = re.search('["\']', prefix)
435 quote = quote_match.group()
436 try:
437 prefix_str = eval(prefix + quote, {})
438 except Exception:
439 return None, []
440
441 token_prefix = re.search('\w*$', prefix).group()
442
443 # TODO: support bytes in Py3k
444 matched = []
445 for key in keys:
446 try:
447 if not key.startswith(prefix_str):
448 continue
449 except (AttributeError, TypeError):
450 # Python 3+ TypeError on b'a'.startswith('a') or vice-versa
451 continue
452
453 # reformat remainder of key to begin with prefix
454 rem = key[len(prefix_str):]
455 # force repr with '
456 rem_repr = repr(rem + '"')
457 if rem_repr.startswith('u') and prefix[0] not in 'uU':
458 try:
459 rem_repr = repr(rem.encode('ascii') + '"')
460 except UnicodeEncodeError:
461 continue
462
463 rem_repr = rem_repr[1 + rem_repr.index("'"):-2]
464 if quote == '"':
465 rem_repr = rem_repr.replace('"', '\\"')
466
467 # then reinsert prefix from start of token
468 matched.append('%s%s' % (token_prefix, rem_repr))
469 return quote, matched
470
471
472
429 class IPCompleter(Completer):
473 class IPCompleter(Completer):
430 """Extension of the completer class with IPython-specific features"""
474 """Extension of the completer class with IPython-specific features"""
431
475
@@ -538,6 +582,7 b' class IPCompleter(Completer):'
538 self.file_matches,
582 self.file_matches,
539 self.magic_matches,
583 self.magic_matches,
540 self.python_func_kw_matches,
584 self.python_func_kw_matches,
585 self.dict_key_matches,
541 ]
586 ]
542
587
543 def all_completions(self, text):
588 def all_completions(self, text):
@@ -804,6 +849,74 b' class IPCompleter(Completer):'
804 argMatches.append("%s=" %namedArg)
849 argMatches.append("%s=" %namedArg)
805 return argMatches
850 return argMatches
806
851
852 def dict_key_matches(self, text):
853 def get_keys(obj):
854 if not callable(getattr(obj, '__getitem__', None)):
855 return []
856 if hasattr(obj, 'keys'):
857 try:
858 return list(obj.keys())
859 except Exception:
860 return []
861 return getattr(getattr(obj, 'dtype', None), 'names', [])
862
863 try:
864 regexps = self.__dict_key_regexps
865 except AttributeError:
866 dict_key_re_fmt = r'''(?x)
867 ( # match dict-referring expression wrt greedy setting
868 %s
869 )
870 \[ # open bracket
871 \s* # and optional whitespace
872 ([uUbB]? # string prefix (r not handled)
873 (?: # unclosed string
874 '(?:[^']|(?<!\\)\\')*
875 |
876 "(?:[^"]|(?<!\\)\\")*
877 )
878 )?
879 $
880 '''
881 regexps = self.__dict_key_regexps = {
882 False: re.compile(dict_key_re_fmt % '''
883 # identifiers separated by .
884 (?!\d)\w+
885 (?:\.(?!\d)\w+)*
886 '''),
887 True: re.compile(dict_key_re_fmt % '''
888 .+
889 ''')
890 }
891
892 match = regexps[self.greedy].search(self.text_until_cursor)
893 if match is None:
894 return []
895
896 expr, prefix = match.groups()
897 try:
898 obj = eval(expr, self.namespace)
899 except Exception:
900 try:
901 obj = eval(expr, self.global_namespace)
902 except Exception:
903 return []
904
905 keys = get_keys(obj)
906 if not keys:
907 return keys
908 closing_quote, matches = match_dict_keys(keys, prefix)
909
910 # append closing quote and bracket as appropriate
911 continuation = self.line_buffer[len(self.text_until_cursor):]
912 if closing_quote and continuation.startswith(closing_quote):
913 suf = ''
914 elif continuation.startswith(']'):
915 suf = closing_quote or ''
916 else:
917 suf = (closing_quote or '') + ']'
918 return [k + suf for k in matches]
919
807 def dispatch_custom_completer(self, text):
920 def dispatch_custom_completer(self, text):
808 #io.rprint("Custom! '%s' %s" % (text, self.custom_completers)) # dbg
921 #io.rprint("Custom! '%s' %s" % (text, self.custom_completers)) # dbg
809 line = self.line_buffer
922 line = self.line_buffer
@@ -20,6 +20,7 b' from IPython.utils.tempdir import TemporaryDirectory'
20 from IPython.utils.generics import complete_object
20 from IPython.utils.generics import complete_object
21 from IPython.utils import py3compat
21 from IPython.utils import py3compat
22 from IPython.utils.py3compat import string_types, unicode_type
22 from IPython.utils.py3compat import string_types, unicode_type
23 from IPython.testing import decorators as dec
23
24
24 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
25 # Test functions
26 # Test functions
@@ -392,3 +393,206 b' def test_magic_completion_order():'
392 text, matches = c.complete('timeit')
393 text, matches = c.complete('timeit')
393 nt.assert_equal(matches, ["timeit", "%timeit","%%timeit"])
394 nt.assert_equal(matches, ["timeit", "%timeit","%%timeit"])
394
395
396
397
398 def test_dict_key_completion_string():
399 """Test dictionary key completion for string keys"""
400 ip = get_ipython()
401 complete = ip.Completer.complete
402
403 ip.user_ns['d'] = {'abc': None}
404
405 # check completion at different stages
406 _, matches = complete(line_buffer="d[")
407 nt.assert_in("'abc']", matches)
408
409 _, matches = complete(line_buffer="d['")
410 nt.assert_in("abc']", matches)
411
412 _, matches = complete(line_buffer="d['a")
413 nt.assert_in("abc']", matches)
414
415 # check use of different quoting
416 _, matches = complete(line_buffer="d[\"")
417 nt.assert_in("abc\"]", matches)
418
419 _, matches = complete(line_buffer="d[\"a")
420 nt.assert_in("abc\"]", matches)
421
422 # check sensitivity to following context
423 _, matches = complete(line_buffer="d[]", cursor_pos=2)
424 nt.assert_in("'abc'", matches)
425
426 _, matches = complete(line_buffer="d['']", cursor_pos=3)
427 nt.assert_in("abc", matches)
428
429 # check multiple solutions are correctly returned and that noise is not
430 ip.user_ns['d'] = {'abc': None, 'abd': None, 'bad': None, object(): None,
431 5: None}
432
433 _, matches = complete(line_buffer="d['a")
434 nt.assert_in("abc']", matches)
435 nt.assert_in("abd']", matches)
436 nt.assert_not_in("bad']", matches)
437
438 # check escaping and whitespace
439 ip.user_ns['d'] = {'a\nb': None, 'a\'b': None, 'a"b': None, 'a word': None}
440 _, matches = complete(line_buffer="d['a")
441 nt.assert_in("a\\nb']", matches)
442 nt.assert_in("a\\'b']", matches)
443 nt.assert_in("a\"b']", matches)
444 nt.assert_in("a word']", matches)
445
446 # - can complete on non-initial word of the string
447 _, matches = complete(line_buffer="d['a w")
448 nt.assert_in("word']", matches)
449
450 # - understands quote escaping
451 _, matches = complete(line_buffer="d['a\\'")
452 nt.assert_in("b']", matches)
453
454 # - default quoting should work like repr
455 _, matches = complete(line_buffer="d[")
456 nt.assert_in("\"a'b\"]", matches)
457
458 # - when opening quote with ", possible to match with unescaped apostrophe
459 _, matches = complete(line_buffer="d[\"a'")
460 nt.assert_in("b\"]", matches)
461
462
463 def test_dict_key_completion_contexts():
464 """Test expression contexts in which dict key completion occurs"""
465 ip = get_ipython()
466 complete = ip.Completer.complete
467 d = {'abc': None}
468 ip.user_ns['d'] = d
469
470 class C:
471 data = d
472 ip.user_ns['C'] = C
473 ip.user_ns['get'] = lambda: d
474
475 def assert_no_completion(**kwargs):
476 _, matches = complete(**kwargs)
477 nt.assert_not_in('abc', matches)
478 nt.assert_not_in('abc\'', matches)
479 nt.assert_not_in('abc\']', matches)
480 nt.assert_not_in('\'abc\'', matches)
481 nt.assert_not_in('\'abc\']', matches)
482
483 def assert_completion(**kwargs):
484 _, matches = complete(**kwargs)
485 nt.assert_in("'abc']", matches)
486
487 # no completion after string closed, even if reopened
488 ip.Completer.greedy = False
489 assert_no_completion(line_buffer="d['a'")
490 assert_no_completion(line_buffer="d[\"a\"")
491 assert_no_completion(line_buffer="d['a' + ")
492 assert_no_completion(line_buffer="d['a' + '")
493
494 # completion in non-trivial expressions
495 assert_completion(line_buffer="+ d[")
496 assert_completion(line_buffer="(d[")
497 assert_completion(line_buffer="C.data[")
498
499 # greedy flag
500 assert_no_completion(line_buffer="get()[")
501 ip.Completer.greedy = True
502 assert_completion(line_buffer="get()[")
503
504
505
506 @dec.onlyif(sys.version_info[0] >= 3, 'This test only applies in Py>=3')
507 def test_dict_key_completion_bytes():
508 """Test handling of bytes in dict key completion"""
509 ip = get_ipython()
510 complete = ip.Completer.complete
511
512 ip.user_ns['d'] = {'abc': None, b'abd': None}
513
514 _, matches = complete(line_buffer="d[")
515 nt.assert_in("'abc']", matches)
516 nt.assert_in("b'abd']", matches)
517
518 if False: # not currently implemented
519 _, matches = complete(line_buffer="d[b")
520 nt.assert_in("b'abd']", matches)
521 nt.assert_not_in("b'abc']", matches)
522
523 _, matches = complete(line_buffer="d[b'")
524 nt.assert_in("abd']", matches)
525 nt.assert_not_in("abc']", matches)
526
527 _, matches = complete(line_buffer="d[B'")
528 nt.assert_in("abd']", matches)
529 nt.assert_not_in("abc']", matches)
530
531 _, matches = complete(line_buffer="d['")
532 nt.assert_in("abc']", matches)
533 nt.assert_not_in("abd']", matches)
534
535
536 @dec.onlyif(sys.version_info[0] < 3, 'This test only applies in Py<3')
537 def test_dict_key_completion_unicode():
538 """Test handling of unicode in dict key completion"""
539 ip = get_ipython()
540 complete = ip.Completer.complete
541
542 ip.user_ns['d'] = {unicode_type('abc'): None,
543 unicode_type('a\xd7\x90', 'utf8'): None}
544
545 _, matches = complete(line_buffer="d[")
546 nt.assert_in("u'abc']", matches)
547 nt.assert_in("u'a\\u05d0']", matches)
548
549 _, matches = complete(line_buffer="d['a")
550 nt.assert_in("abc']", matches)
551 nt.assert_not_in("a\\u05d0']", matches)
552
553 _, matches = complete(line_buffer="d[u'a")
554 nt.assert_in("abc']", matches)
555 nt.assert_in("a\\u05d0']", matches)
556
557 _, matches = complete(line_buffer="d[U'a")
558 nt.assert_in("abc']", matches)
559 nt.assert_in("a\\u05d0']", matches)
560
561
562 def test_dict_like_key_completion():
563 """Test dict key completion applies where __getitem__ and keys exist"""
564 class D(object):
565 def __getitem__(self):
566 pass
567 def keys(self):
568 return iter(['hello', 'world'])
569 ip = get_ipython()
570 complete = ip.Completer.complete
571 ip.user_ns['d'] = D()
572 _, matches = complete(line_buffer="d['")
573 nt.assert_in("hello']", matches)
574 nt.assert_in("world']", matches)
575
576
577 @dec.skip_without('numpy')
578 def test_struct_array_key_completion():
579 """Test dict key completion applies to numpy struct arrays"""
580 import numpy
581 ip = get_ipython()
582 complete = ip.Completer.complete
583 ip.user_ns['d'] = numpy.array([], dtype=[('hello', 'f'), ('world', 'f')])
584 _, matches = complete(line_buffer="d['")
585 nt.assert_in("hello']", matches)
586 nt.assert_in("world']", matches)
587
588
589 @dec.skip_without('pandas')
590 def test_dataframe_key_completion():
591 """Test dict key completion applies to pandas DataFrames"""
592 import pandas
593 ip = get_ipython()
594 complete = ip.Completer.complete
595 ip.user_ns['d'] = pandas.DataFrame({'hello': [1], 'world': [2]})
596 _, matches = complete(line_buffer="d['")
597 nt.assert_in("hello']", matches)
598 nt.assert_in("world']", matches)
General Comments 0
You need to be logged in to leave comments. Login now