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