Show More
@@ -753,7 +753,8 b' def get__all__entries(obj):' | |||
|
753 | 753 | return [w for w in words if isinstance(w, str)] |
|
754 | 754 | |
|
755 | 755 | |
|
756 |
def match_dict_keys(keys: List[Union[str, bytes]], prefix: str, delims: str |
|
|
756 | def match_dict_keys(keys: List[Union[str, bytes, Tuple[Union[str, bytes]]]], prefix: str, delims: str, | |
|
757 | extra_prefix: Optional[Tuple[str, bytes]]=None) -> Tuple[str, int, List[str]]: | |
|
757 | 758 | """Used by dict_key_matches, matching the prefix to a list of keys |
|
758 | 759 | |
|
759 | 760 | Parameters |
@@ -761,9 +762,12 b' def match_dict_keys(keys: List[Union[str, bytes]], prefix: str, delims: str) -> ' | |||
|
761 | 762 | keys: |
|
762 | 763 | list of keys in dictionary currently being completed. |
|
763 | 764 | prefix: |
|
764 |
Part of the text already typed by the user. |
|
|
765 | Part of the text already typed by the user. E.g. `mydict[b'fo` | |
|
765 | 766 | delims: |
|
766 | 767 | String of delimiters to consider when finding the current key. |
|
768 | extra_prefix: optional | |
|
769 | Part of the text already typed in multi-key index cases. E.g. for | |
|
770 | `mydict['foo', "bar", 'b`, this would be `('foo', 'bar')`. | |
|
767 | 771 | |
|
768 | 772 | Returns |
|
769 | 773 | ======= |
@@ -774,10 +778,37 b' def match_dict_keys(keys: List[Union[str, bytes]], prefix: str, delims: str) -> ' | |||
|
774 | 778 | ``matches`` a list of replacement/completion |
|
775 | 779 | |
|
776 | 780 | """ |
|
777 | keys = [k for k in keys if isinstance(k, (str, bytes))] | |
|
781 | prefix_tuple = extra_prefix if extra_prefix else () | |
|
782 | Nprefix = len(prefix_tuple) | |
|
783 | def filter_prefix_tuple(key): | |
|
784 | # Reject too short keys | |
|
785 | if len(key) <= Nprefix: | |
|
786 | return False | |
|
787 | # Reject keys with non str/bytes in it | |
|
788 | for k in key: | |
|
789 | if not isinstance(k, (str, bytes)): | |
|
790 | return False | |
|
791 | # Reject keys that do not match the prefix | |
|
792 | for k, pt in zip(key, prefix_tuple): | |
|
793 | if k != pt: | |
|
794 | return False | |
|
795 | # All checks passed! | |
|
796 | return True | |
|
797 | ||
|
798 | filtered_keys:List[Union[str,bytes]] = [] | |
|
799 | def _add_to_filtered_keys(key): | |
|
800 | if isinstance(key, (str, bytes)): | |
|
801 | filtered_keys.append(key) | |
|
802 | ||
|
803 | for k in keys: | |
|
804 | if isinstance(k, tuple): | |
|
805 | if filter_prefix_tuple(k): | |
|
806 | _add_to_filtered_keys(k[Nprefix]) | |
|
807 | else: | |
|
808 | _add_to_filtered_keys(k) | |
|
809 | ||
|
778 | 810 | if not prefix: |
|
779 | return '', 0, [repr(k) for k in keys | |
|
780 | if isinstance(k, (str, bytes))] | |
|
811 | return '', 0, [repr(k) for k in filtered_keys] | |
|
781 | 812 | quote_match = re.search('["\']', prefix) |
|
782 | 813 | assert quote_match is not None # silence mypy |
|
783 | 814 | quote = quote_match.group() |
@@ -793,7 +824,7 b' def match_dict_keys(keys: List[Union[str, bytes]], prefix: str, delims: str) -> ' | |||
|
793 | 824 | token_prefix = token_match.group() |
|
794 | 825 | |
|
795 | 826 | matched:List[str] = [] |
|
796 | for key in keys: | |
|
827 | for key in filtered_keys: | |
|
797 | 828 | try: |
|
798 | 829 | if not key.startswith(prefix_str): |
|
799 | 830 | continue |
@@ -1641,6 +1672,15 b' class IPCompleter(Completer):' | |||
|
1641 | 1672 | ) |
|
1642 | 1673 | \[ # open bracket |
|
1643 | 1674 | \s* # and optional whitespace |
|
1675 | # Capture any number of str-like objects (e.g. "a", "b", 'c') | |
|
1676 | ((?:[uUbB]? # string prefix (r not handled) | |
|
1677 | (?: | |
|
1678 | '(?:[^']|(?<!\\)\\')*' | |
|
1679 | | | |
|
1680 | "(?:[^"]|(?<!\\)\\")*" | |
|
1681 | ) | |
|
1682 | \s*,\s* | |
|
1683 | )*) | |
|
1644 | 1684 | ([uUbB]? # string prefix (r not handled) |
|
1645 | 1685 | (?: # unclosed string |
|
1646 | 1686 | '(?:[^']|(?<!\\)\\')* |
@@ -1662,10 +1702,11 b' class IPCompleter(Completer):' | |||
|
1662 | 1702 | } |
|
1663 | 1703 | |
|
1664 | 1704 | match = regexps[self.greedy].search(self.text_until_cursor) |
|
1705 | ||
|
1665 | 1706 | if match is None: |
|
1666 | 1707 | return [] |
|
1667 | 1708 | |
|
1668 | expr, prefix = match.groups() | |
|
1709 | expr, prefix0, prefix = match.groups() | |
|
1669 | 1710 | try: |
|
1670 | 1711 | obj = eval(expr, self.namespace) |
|
1671 | 1712 | except Exception: |
@@ -1677,7 +1718,10 b' class IPCompleter(Completer):' | |||
|
1677 | 1718 | keys = self._get_keys(obj) |
|
1678 | 1719 | if not keys: |
|
1679 | 1720 | return keys |
|
1680 | closing_quote, token_offset, matches = match_dict_keys(keys, prefix, self.splitter.delims) | |
|
1721 | ||
|
1722 | extra_prefix = eval(prefix0) if prefix0 != '' else None | |
|
1723 | ||
|
1724 | closing_quote, token_offset, matches = match_dict_keys(keys, prefix, self.splitter.delims, extra_prefix=extra_prefix) | |
|
1681 | 1725 | if not matches: |
|
1682 | 1726 | return matches |
|
1683 | 1727 | |
@@ -1687,7 +1731,7 b' class IPCompleter(Completer):' | |||
|
1687 | 1731 | # - the start of the completion |
|
1688 | 1732 | text_start = len(self.text_until_cursor) - len(text) |
|
1689 | 1733 | if prefix: |
|
1690 |
key_start = match.start( |
|
|
1734 | key_start = match.start(3) | |
|
1691 | 1735 | completion_start = key_start + token_offset |
|
1692 | 1736 | else: |
|
1693 | 1737 | key_start = completion_start = match.end() |
@@ -2089,7 +2133,7 b' class IPCompleter(Completer):' | |||
|
2089 | 2133 | |
|
2090 | 2134 | # Start with a clean slate of completions |
|
2091 | 2135 | matches = [] |
|
2092 | ||
|
2136 | ||
|
2093 | 2137 | # FIXME: we should extend our api to return a dict with completions for |
|
2094 | 2138 | # different types of objects. The rlcomplete() method could then |
|
2095 | 2139 | # simply collapse the dict into a list for readline, but we'd have |
@@ -846,6 +846,35 b' class TestCompleter(unittest.TestCase):' | |||
|
846 | 846 | |
|
847 | 847 | match_dict_keys |
|
848 | 848 | |
|
849 | def test_match_dict_keys_tuple(self): | |
|
850 | """ | |
|
851 | Test that match_dict_keys called with extra prefix works on a couple of use case, | |
|
852 | does return what expected, and does not crash. | |
|
853 | """ | |
|
854 | delims = " \t\n`!@#$^&*()=+[{]}\\|;:'\",<>?" | |
|
855 | ||
|
856 | keys = [("foo", "bar"), ("foo", "oof"), ("foo", b"bar"), ('other', 'test')] | |
|
857 | ||
|
858 | # Completion on first key == "foo" | |
|
859 | assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["bar", "oof"]) | |
|
860 | assert match_dict_keys(keys, "\"", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["bar", "oof"]) | |
|
861 | assert match_dict_keys(keys, "'o", delims=delims, extra_prefix=("foo",)) == ("'", 1, ["oof"]) | |
|
862 | assert match_dict_keys(keys, "\"o", delims=delims, extra_prefix=("foo",)) == ("\"", 1, ["oof"]) | |
|
863 | assert match_dict_keys(keys, "b'", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) | |
|
864 | assert match_dict_keys(keys, "b\"", delims=delims, extra_prefix=("foo",)) == ("\"", 2, ["bar"]) | |
|
865 | assert match_dict_keys(keys, "b'b", delims=delims, extra_prefix=("foo",)) == ("'", 2, ["bar"]) | |
|
866 | assert match_dict_keys(keys, "b\"b", delims=delims, extra_prefix=("foo",)) == ("\"", 2, ["bar"]) | |
|
867 | ||
|
868 | # No Completion | |
|
869 | assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("no_foo",)) == ("'", 1, []) | |
|
870 | assert match_dict_keys(keys, "'", delims=delims, extra_prefix=("fo",)) == ("'", 1, []) | |
|
871 | ||
|
872 | keys = [('foo1', 'foo2', 'foo3', 'foo4'), ('foo1', 'foo2', 'bar', 'foo4')] | |
|
873 | assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1',)) == ("'", 1, ["foo2", "foo2"]) | |
|
874 | assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2')) == ("'", 1, ["foo3"]) | |
|
875 | assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3')) == ("'", 1, ["foo4"]) | |
|
876 | assert match_dict_keys(keys, "'foo", delims=delims, extra_prefix=('foo1', 'foo2', 'foo3', 'foo4')) == ("'", 1, []) | |
|
877 | ||
|
849 | 878 | def test_dict_key_completion_string(self): |
|
850 | 879 | """Test dictionary key completion for string keys""" |
|
851 | 880 | ip = get_ipython() |
@@ -891,12 +920,16 b' class TestCompleter(unittest.TestCase):' | |||
|
891 | 920 | "bad": None, |
|
892 | 921 | object(): None, |
|
893 | 922 | 5: None, |
|
923 | ("abe", None): None, | |
|
924 | (None, "abf"): None | |
|
894 | 925 | } |
|
895 | 926 | |
|
896 | 927 | _, matches = complete(line_buffer="d['a") |
|
897 | 928 | nt.assert_in("abc", matches) |
|
898 | 929 | nt.assert_in("abd", matches) |
|
899 | 930 | nt.assert_not_in("bad", matches) |
|
931 | nt.assert_not_in("abe", matches) | |
|
932 | nt.assert_not_in("abf", matches) | |
|
900 | 933 | assert not any(m.endswith(("]", '"', "'")) for m in matches), matches |
|
901 | 934 | |
|
902 | 935 | # check escaping and whitespace |
@@ -930,6 +963,74 b' class TestCompleter(unittest.TestCase):' | |||
|
930 | 963 | _, matches = complete(line_buffer="d['before-af") |
|
931 | 964 | nt.assert_in("before-after", matches) |
|
932 | 965 | |
|
966 | # check completion on tuple-of-string keys at different stage - on first key | |
|
967 | ip.user_ns["d"] = {('foo', 'bar'): None} | |
|
968 | _, matches = complete(line_buffer="d[") | |
|
969 | nt.assert_in("'foo'", matches) | |
|
970 | nt.assert_not_in("'foo']", matches) | |
|
971 | nt.assert_not_in("'bar'", matches) | |
|
972 | nt.assert_not_in("foo", matches) | |
|
973 | nt.assert_not_in("bar", matches) | |
|
974 | ||
|
975 | # - match the prefix | |
|
976 | _, matches = complete(line_buffer="d['f") | |
|
977 | nt.assert_in("foo", matches) | |
|
978 | nt.assert_not_in("foo']", matches) | |
|
979 | nt.assert_not_in("foo\"]", matches) | |
|
980 | _, matches = complete(line_buffer="d['foo") | |
|
981 | nt.assert_in("foo", matches) | |
|
982 | ||
|
983 | # - can complete on second key | |
|
984 | _, matches = complete(line_buffer="d['foo', ") | |
|
985 | nt.assert_in("'bar'", matches) | |
|
986 | _, matches = complete(line_buffer="d['foo', 'b") | |
|
987 | nt.assert_in("bar", matches) | |
|
988 | nt.assert_not_in("foo", matches) | |
|
989 | ||
|
990 | # - does not propose missing keys | |
|
991 | _, matches = complete(line_buffer="d['foo', 'f") | |
|
992 | nt.assert_not_in("bar", matches) | |
|
993 | nt.assert_not_in("foo", matches) | |
|
994 | ||
|
995 | # check sensitivity to following context | |
|
996 | _, matches = complete(line_buffer="d['foo',]", cursor_pos=8) | |
|
997 | nt.assert_in("'bar'", matches) | |
|
998 | nt.assert_not_in("bar", matches) | |
|
999 | nt.assert_not_in("'foo'", matches) | |
|
1000 | nt.assert_not_in("foo", matches) | |
|
1001 | ||
|
1002 | _, matches = complete(line_buffer="d['']", cursor_pos=3) | |
|
1003 | nt.assert_in("foo", matches) | |
|
1004 | assert not any(m.endswith(("]", '"', "'")) for m in matches), matches | |
|
1005 | ||
|
1006 | _, matches = complete(line_buffer='d[""]', cursor_pos=3) | |
|
1007 | nt.assert_in("foo", matches) | |
|
1008 | assert not any(m.endswith(("]", '"', "'")) for m in matches), matches | |
|
1009 | ||
|
1010 | _, matches = complete(line_buffer='d["foo","]', cursor_pos=9) | |
|
1011 | nt.assert_in("bar", matches) | |
|
1012 | assert not any(m.endswith(("]", '"', "'")) for m in matches), matches | |
|
1013 | ||
|
1014 | _, matches = complete(line_buffer='d["foo",]', cursor_pos=8) | |
|
1015 | nt.assert_in("'bar'", matches) | |
|
1016 | nt.assert_not_in("bar", matches) | |
|
1017 | ||
|
1018 | # Can complete with longer tuple keys | |
|
1019 | ip.user_ns["d"] = {('foo', 'bar', 'foobar'): None} | |
|
1020 | ||
|
1021 | # - can complete second key | |
|
1022 | _, matches = complete(line_buffer="d['foo', 'b") | |
|
1023 | nt.assert_in('bar', matches) | |
|
1024 | nt.assert_not_in('foo', matches) | |
|
1025 | nt.assert_not_in('foobar', matches) | |
|
1026 | ||
|
1027 | # - can complete third key | |
|
1028 | _, matches = complete(line_buffer="d['foo', 'bar', 'fo") | |
|
1029 | nt.assert_in('foobar', matches) | |
|
1030 | nt.assert_not_in('foo', matches) | |
|
1031 | nt.assert_not_in('bar', matches) | |
|
1032 | ||
|
1033 | ||
|
933 | 1034 | def test_dict_key_completion_contexts(self): |
|
934 | 1035 | """Test expression contexts in which dict key completion occurs""" |
|
935 | 1036 | ip = get_ipython() |
General Comments 0
You need to be logged in to leave comments.
Login now