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