Show More
@@ -753,7 +753,8 b' def get__all__entries(obj):' | |||||
753 | return [w for w in words if isinstance(w, str)] |
|
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 | """Used by dict_key_matches, matching the prefix to a list of keys |
|
758 | """Used by dict_key_matches, matching the prefix to a list of keys | |
758 |
|
759 | |||
759 | Parameters |
|
760 | Parameters | |
@@ -761,9 +762,12 b' def match_dict_keys(keys: List[Union[str, bytes]], prefix: str, delims: str) -> ' | |||||
761 | keys: |
|
762 | keys: | |
762 | list of keys in dictionary currently being completed. |
|
763 | list of keys in dictionary currently being completed. | |
763 | prefix: |
|
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 | delims: |
|
766 | delims: | |
766 | String of delimiters to consider when finding the current key. |
|
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 | Returns |
|
772 | Returns | |
769 | ======= |
|
773 | ======= | |
@@ -774,10 +778,37 b' def match_dict_keys(keys: List[Union[str, bytes]], prefix: str, delims: str) -> ' | |||||
774 | ``matches`` a list of replacement/completion |
|
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 | if not prefix: |
|
810 | if not prefix: | |
779 | return '', 0, [repr(k) for k in keys |
|
811 | return '', 0, [repr(k) for k in filtered_keys] | |
780 | if isinstance(k, (str, bytes))] |
|
|||
781 | quote_match = re.search('["\']', prefix) |
|
812 | quote_match = re.search('["\']', prefix) | |
782 | assert quote_match is not None # silence mypy |
|
813 | assert quote_match is not None # silence mypy | |
783 | quote = quote_match.group() |
|
814 | quote = quote_match.group() | |
@@ -793,7 +824,7 b' def match_dict_keys(keys: List[Union[str, bytes]], prefix: str, delims: str) -> ' | |||||
793 | token_prefix = token_match.group() |
|
824 | token_prefix = token_match.group() | |
794 |
|
825 | |||
795 | matched:List[str] = [] |
|
826 | matched:List[str] = [] | |
796 | for key in keys: |
|
827 | for key in filtered_keys: | |
797 | try: |
|
828 | try: | |
798 | if not key.startswith(prefix_str): |
|
829 | if not key.startswith(prefix_str): | |
799 | continue |
|
830 | continue | |
@@ -1641,6 +1672,15 b' class IPCompleter(Completer):' | |||||
1641 | ) |
|
1672 | ) | |
1642 | \[ # open bracket |
|
1673 | \[ # open bracket | |
1643 | \s* # and optional whitespace |
|
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 | ([uUbB]? # string prefix (r not handled) |
|
1684 | ([uUbB]? # string prefix (r not handled) | |
1645 | (?: # unclosed string |
|
1685 | (?: # unclosed string | |
1646 | '(?:[^']|(?<!\\)\\')* |
|
1686 | '(?:[^']|(?<!\\)\\')* | |
@@ -1662,10 +1702,11 b' class IPCompleter(Completer):' | |||||
1662 | } |
|
1702 | } | |
1663 |
|
1703 | |||
1664 | match = regexps[self.greedy].search(self.text_until_cursor) |
|
1704 | match = regexps[self.greedy].search(self.text_until_cursor) | |
|
1705 | ||||
1665 | if match is None: |
|
1706 | if match is None: | |
1666 | return [] |
|
1707 | return [] | |
1667 |
|
1708 | |||
1668 | expr, prefix = match.groups() |
|
1709 | expr, prefix0, prefix = match.groups() | |
1669 | try: |
|
1710 | try: | |
1670 | obj = eval(expr, self.namespace) |
|
1711 | obj = eval(expr, self.namespace) | |
1671 | except Exception: |
|
1712 | except Exception: | |
@@ -1677,7 +1718,10 b' class IPCompleter(Completer):' | |||||
1677 | keys = self._get_keys(obj) |
|
1718 | keys = self._get_keys(obj) | |
1678 | if not keys: |
|
1719 | if not keys: | |
1679 | return keys |
|
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 | if not matches: |
|
1725 | if not matches: | |
1682 | return matches |
|
1726 | return matches | |
1683 |
|
1727 | |||
@@ -1687,7 +1731,7 b' class IPCompleter(Completer):' | |||||
1687 | # - the start of the completion |
|
1731 | # - the start of the completion | |
1688 | text_start = len(self.text_until_cursor) - len(text) |
|
1732 | text_start = len(self.text_until_cursor) - len(text) | |
1689 | if prefix: |
|
1733 | if prefix: | |
1690 |
key_start = match.start( |
|
1734 | key_start = match.start(3) | |
1691 | completion_start = key_start + token_offset |
|
1735 | completion_start = key_start + token_offset | |
1692 | else: |
|
1736 | else: | |
1693 | key_start = completion_start = match.end() |
|
1737 | key_start = completion_start = match.end() | |
@@ -2089,7 +2133,7 b' class IPCompleter(Completer):' | |||||
2089 |
|
2133 | |||
2090 | # Start with a clean slate of completions |
|
2134 | # Start with a clean slate of completions | |
2091 | matches = [] |
|
2135 | matches = [] | |
2092 |
|
2136 | |||
2093 | # FIXME: we should extend our api to return a dict with completions for |
|
2137 | # FIXME: we should extend our api to return a dict with completions for | |
2094 | # different types of objects. The rlcomplete() method could then |
|
2138 | # different types of objects. The rlcomplete() method could then | |
2095 | # simply collapse the dict into a list for readline, but we'd have |
|
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 | match_dict_keys |
|
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 | def test_dict_key_completion_string(self): |
|
878 | def test_dict_key_completion_string(self): | |
850 | """Test dictionary key completion for string keys""" |
|
879 | """Test dictionary key completion for string keys""" | |
851 | ip = get_ipython() |
|
880 | ip = get_ipython() | |
@@ -891,12 +920,16 b' class TestCompleter(unittest.TestCase):' | |||||
891 | "bad": None, |
|
920 | "bad": None, | |
892 | object(): None, |
|
921 | object(): None, | |
893 | 5: None, |
|
922 | 5: None, | |
|
923 | ("abe", None): None, | |||
|
924 | (None, "abf"): None | |||
894 | } |
|
925 | } | |
895 |
|
926 | |||
896 | _, matches = complete(line_buffer="d['a") |
|
927 | _, matches = complete(line_buffer="d['a") | |
897 | nt.assert_in("abc", matches) |
|
928 | nt.assert_in("abc", matches) | |
898 | nt.assert_in("abd", matches) |
|
929 | nt.assert_in("abd", matches) | |
899 | nt.assert_not_in("bad", matches) |
|
930 | nt.assert_not_in("bad", matches) | |
|
931 | nt.assert_not_in("abe", matches) | |||
|
932 | nt.assert_not_in("abf", matches) | |||
900 | assert not any(m.endswith(("]", '"', "'")) for m in matches), matches |
|
933 | assert not any(m.endswith(("]", '"', "'")) for m in matches), matches | |
901 |
|
934 | |||
902 | # check escaping and whitespace |
|
935 | # check escaping and whitespace | |
@@ -930,6 +963,74 b' class TestCompleter(unittest.TestCase):' | |||||
930 | _, matches = complete(line_buffer="d['before-af") |
|
963 | _, matches = complete(line_buffer="d['before-af") | |
931 | nt.assert_in("before-after", matches) |
|
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 | def test_dict_key_completion_contexts(self): |
|
1034 | def test_dict_key_completion_contexts(self): | |
934 | """Test expression contexts in which dict key completion occurs""" |
|
1035 | """Test expression contexts in which dict key completion occurs""" | |
935 | ip = get_ipython() |
|
1036 | ip = get_ipython() |
General Comments 0
You need to be logged in to leave comments.
Login now