##// END OF EJS Templates
Merge pull request #5304 from jnothman/dict_key_completion...
Thomas Kluyver -
r16542:4e2e2d46 merge
parent child Browse files
Show More
@@ -96,6 +96,7 b" if sys.platform == 'win32':"
96 else:
96 else:
97 PROTECTABLES = ' ()[]{}?=\\|;:\'#*"^&'
97 PROTECTABLES = ' ()[]{}?=\\|;:\'#*"^&'
98
98
99
99 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
100 # Main functions and classes
101 # Main functions and classes
101 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
@@ -425,6 +426,62 b' def get__all__entries(obj):'
425 return [w for w in words if isinstance(w, string_types)]
426 return [w for w in words if isinstance(w, string_types)]
426
427
427
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, UnicodeError):
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 wrapped in '
456 rem_repr = repr(rem + '"')
457 if rem_repr.startswith('u') and prefix[0] not in 'uU':
458 # Found key is unicode, but prefix is Py2 string.
459 # Therefore attempt to interpret key as string.
460 try:
461 rem_repr = repr(rem.encode('ascii') + '"')
462 except UnicodeEncodeError:
463 continue
464
465 rem_repr = rem_repr[1 + rem_repr.index("'"):-2]
466 if quote == '"':
467 # The entered prefix is quoted with ",
468 # but the match is quoted with '.
469 # A contained " hence needs escaping for comparison:
470 rem_repr = rem_repr.replace('"', '\\"')
471
472 # then reinsert prefix from start of token
473 matched.append('%s%s' % (token_prefix, rem_repr))
474 return quote, matched
475
476
477 def _safe_isinstance(obj, module, class_name):
478 """Checks if obj is an instance of module.class_name if loaded
479 """
480 return (module in sys.modules and
481 isinstance(obj, getattr(__import__(module), class_name)))
482
483
484
428 class IPCompleter(Completer):
485 class IPCompleter(Completer):
429 """Extension of the completer class with IPython-specific features"""
486 """Extension of the completer class with IPython-specific features"""
430
487
@@ -537,6 +594,7 b' class IPCompleter(Completer):'
537 self.file_matches,
594 self.file_matches,
538 self.magic_matches,
595 self.magic_matches,
539 self.python_func_kw_matches,
596 self.python_func_kw_matches,
597 self.dict_key_matches,
540 ]
598 ]
541
599
542 def all_completions(self, text):
600 def all_completions(self, text):
@@ -803,6 +861,76 b' class IPCompleter(Completer):'
803 argMatches.append("%s=" %namedArg)
861 argMatches.append("%s=" %namedArg)
804 return argMatches
862 return argMatches
805
863
864 def dict_key_matches(self, text):
865 def get_keys(obj):
866 # Only allow completion for known in-memory dict-like types
867 if isinstance(obj, dict) or\
868 _safe_isinstance(obj, 'pandas', 'DataFrame'):
869 try:
870 return list(obj.keys())
871 except Exception:
872 return []
873 elif _safe_isinstance(obj, 'numpy', 'ndarray'):
874 return obj.dtype.names or []
875 return []
876
877 try:
878 regexps = self.__dict_key_regexps
879 except AttributeError:
880 dict_key_re_fmt = r'''(?x)
881 ( # match dict-referring expression wrt greedy setting
882 %s
883 )
884 \[ # open bracket
885 \s* # and optional whitespace
886 ([uUbB]? # string prefix (r not handled)
887 (?: # unclosed string
888 '(?:[^']|(?<!\\)\\')*
889 |
890 "(?:[^"]|(?<!\\)\\")*
891 )
892 )?
893 $
894 '''
895 regexps = self.__dict_key_regexps = {
896 False: re.compile(dict_key_re_fmt % '''
897 # identifiers separated by .
898 (?!\d)\w+
899 (?:\.(?!\d)\w+)*
900 '''),
901 True: re.compile(dict_key_re_fmt % '''
902 .+
903 ''')
904 }
905
906 match = regexps[self.greedy].search(self.text_until_cursor)
907 if match is None:
908 return []
909
910 expr, prefix = match.groups()
911 try:
912 obj = eval(expr, self.namespace)
913 except Exception:
914 try:
915 obj = eval(expr, self.global_namespace)
916 except Exception:
917 return []
918
919 keys = get_keys(obj)
920 if not keys:
921 return keys
922 closing_quote, matches = match_dict_keys(keys, prefix)
923
924 # append closing quote and bracket as appropriate
925 continuation = self.line_buffer[len(self.text_until_cursor):]
926 if closing_quote and continuation.startswith(closing_quote):
927 suf = ''
928 elif continuation.startswith(']'):
929 suf = closing_quote or ''
930 else:
931 suf = (closing_quote or '') + ']'
932 return [k + suf for k in matches]
933
806 def dispatch_custom_completer(self, text):
934 def dispatch_custom_completer(self, text):
807 #io.rprint("Custom! '%s' %s" % (text, self.custom_completers)) # dbg
935 #io.rprint("Custom! '%s' %s" % (text, self.custom_completers)) # dbg
808 line = self.line_buffer
936 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,235 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_py2():
538 """Test handling of unicode in dict key completion"""
539 ip = get_ipython()
540 complete = ip.Completer.complete
541
542 ip.user_ns['d'] = {u'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 # query using escape
562 _, matches = complete(line_buffer="d[u'a\\u05d0")
563 nt.assert_in("u05d0']", matches) # tokenized after \\
564
565 # query using character
566 _, matches = complete(line_buffer=unicode_type("d[u'a\xd7\x90", 'utf8'))
567 nt.assert_in("']", matches)
568
569
570 @dec.onlyif(sys.version_info[0] >= 3, 'This test only applies in Py>=3')
571 def test_dict_key_completion_unicode_py3():
572 """Test handling of unicode in dict key completion"""
573 ip = get_ipython()
574 complete = ip.Completer.complete
575
576 ip.user_ns['d'] = {unicode_type(b'a\xd7\x90', 'utf8'): None}
577
578 # query using escape
579 _, matches = complete(line_buffer="d['a\\u05d0")
580 nt.assert_in("u05d0']", matches) # tokenized after \\
581
582 # query using character
583 _, matches = complete(line_buffer=unicode_type(b"d['a\xd7\x90", 'utf8'))
584 nt.assert_in(unicode_type(b"a\xd7\x90']", 'utf8'), matches)
585
586
587 @dec.skip_without('numpy')
588 def test_struct_array_key_completion():
589 """Test dict key completion applies to numpy struct arrays"""
590 import numpy
591 ip = get_ipython()
592 complete = ip.Completer.complete
593 ip.user_ns['d'] = numpy.array([], dtype=[('hello', 'f'), ('world', 'f')])
594 _, matches = complete(line_buffer="d['")
595 nt.assert_in("hello']", matches)
596 nt.assert_in("world']", matches)
597
598
599 @dec.skip_without('pandas')
600 def test_dataframe_key_completion():
601 """Test dict key completion applies to pandas DataFrames"""
602 import pandas
603 ip = get_ipython()
604 complete = ip.Completer.complete
605 ip.user_ns['d'] = pandas.DataFrame({'hello': [1], 'world': [2]})
606 _, matches = complete(line_buffer="d['")
607 nt.assert_in("hello']", matches)
608 nt.assert_in("world']", matches)
609
610
611 def test_dict_key_completion_invalids():
612 """Smoke test cases dict key completion can't handle"""
613 ip = get_ipython()
614 complete = ip.Completer.complete
615
616 ip.user_ns['no_getitem'] = None
617 ip.user_ns['no_keys'] = []
618 ip.user_ns['cant_call_keys'] = dict
619 ip.user_ns['empty'] = {}
620 ip.user_ns['d'] = {'abc': 5}
621
622 _, matches = complete(line_buffer="no_getitem['")
623 _, matches = complete(line_buffer="no_keys['")
624 _, matches = complete(line_buffer="cant_call_keys['")
625 _, matches = complete(line_buffer="empty['")
626 _, matches = complete(line_buffer="name_error['")
627 _, matches = complete(line_buffer="d['\\") # incomplete escape
General Comments 0
You need to be logged in to leave comments. Login now