Show More
@@ -96,6 +96,7 b" if sys.platform == 'win32':" | |||
|
96 | 96 | else: |
|
97 | 97 | PROTECTABLES = ' ()[]{}?=\\|;:\'#*"^&' |
|
98 | 98 | |
|
99 | ||
|
99 | 100 | #----------------------------------------------------------------------------- |
|
100 | 101 | # Main functions and classes |
|
101 | 102 | #----------------------------------------------------------------------------- |
@@ -425,6 +426,62 b' def get__all__entries(obj):' | |||
|
425 | 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 | 485 | class IPCompleter(Completer): |
|
429 | 486 | """Extension of the completer class with IPython-specific features""" |
|
430 | 487 | |
@@ -537,6 +594,7 b' class IPCompleter(Completer):' | |||
|
537 | 594 | self.file_matches, |
|
538 | 595 | self.magic_matches, |
|
539 | 596 | self.python_func_kw_matches, |
|
597 | self.dict_key_matches, | |
|
540 | 598 | ] |
|
541 | 599 | |
|
542 | 600 | def all_completions(self, text): |
@@ -803,6 +861,76 b' class IPCompleter(Completer):' | |||
|
803 | 861 | argMatches.append("%s=" %namedArg) |
|
804 | 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 | 934 | def dispatch_custom_completer(self, text): |
|
807 | 935 | #io.rprint("Custom! '%s' %s" % (text, self.custom_completers)) # dbg |
|
808 | 936 | line = self.line_buffer |
@@ -20,6 +20,7 b' from IPython.utils.tempdir import TemporaryDirectory' | |||
|
20 | 20 | from IPython.utils.generics import complete_object |
|
21 | 21 | from IPython.utils import py3compat |
|
22 | 22 | from IPython.utils.py3compat import string_types, unicode_type |
|
23 | from IPython.testing import decorators as dec | |
|
23 | 24 | |
|
24 | 25 | #----------------------------------------------------------------------------- |
|
25 | 26 | # Test functions |
@@ -392,3 +393,235 b' def test_magic_completion_order():' | |||
|
392 | 393 | text, matches = c.complete('timeit') |
|
393 | 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