1 import os 2 3 from jedi.api import classes 4 from jedi.api.strings import StringName, get_quote_ending 5 from jedi.api.helpers import match 6 from jedi.inference.helpers import get_str_or_none 7 8 9 class PathName(StringName): 10 api_type = 'path' 11 12 13 def complete_file_name(inference_state, module_context, start_leaf, quote, string, 14 like_name, signatures_callback, code_lines, position, fuzzy): 15 # First we want to find out what can actually be changed as a name. 16 like_name_length = len(os.path.basename(string)) 17 18 addition = _get_string_additions(module_context, start_leaf) 19 if string.startswith('~'): 20 string = os.path.expanduser(string) 21 if addition is None: 22 return 23 string = addition + string 24 25 # Here we use basename again, because if strings are added like 26 # `'foo' + 'bar`, it should complete to `foobar/`. 27 must_start_with = os.path.basename(string) 28 string = os.path.dirname(string) 29 30 sigs = signatures_callback(*position) 31 is_in_os_path_join = sigs and all(s.full_name == 'os.path.join' for s in sigs) 32 if is_in_os_path_join: 33 to_be_added = _add_os_path_join(module_context, start_leaf, sigs[0].bracket_start) 34 if to_be_added is None: 35 is_in_os_path_join = False 36 else: 37 string = to_be_added + string 38 base_path = os.path.join(inference_state.project.path, string) 39 try: 40 listed = sorted(os.scandir(base_path), key=lambda e: e.name) 41 # OSError: [Errno 36] File name too long: '...' 42 except (FileNotFoundError, OSError): 43 return 44 quote_ending = get_quote_ending(quote, code_lines, position) 45 for entry in listed: 46 name = entry.name 47 if match(name, must_start_with, fuzzy=fuzzy): 48 if is_in_os_path_join or not entry.is_dir(): 49 name += quote_ending 50 else: 51 name += os.path.sep 52 53 yield classes.Completion( 54 inference_state, 55 PathName(inference_state, name[len(must_start_with) - like_name_length:]), 56 stack=None, 57 like_name_length=like_name_length, 58 is_fuzzy=fuzzy, 59 ) 60 61 62 def _get_string_additions(module_context, start_leaf): 63 def iterate_nodes(): 64 node = addition.parent 65 was_addition = True 66 for child_node in reversed(node.children[:node.children.index(addition)]): 67 if was_addition: 68 was_addition = False 69 yield child_node 70 continue 71 72 if child_node != '+': 73 break 74 was_addition = True 75 76 addition = start_leaf.get_previous_leaf() 77 if addition != '+': 78 return '' 79 context = module_context.create_context(start_leaf) 80 return _add_strings(context, reversed(list(iterate_nodes()))) 81 82 83 def _add_strings(context, nodes, add_slash=False): 84 string = '' 85 first = True 86 for child_node in nodes: 87 values = context.infer_node(child_node) 88 if len(values) != 1: 89 return None 90 c, = values 91 s = get_str_or_none(c) 92 if s is None: 93 return None 94 if not first and add_slash: 95 string += os.path.sep 96 string += s 97 first = False 98 return string 99 100 101 def _add_os_path_join(module_context, start_leaf, bracket_start): 102 def check(maybe_bracket, nodes): 103 if maybe_bracket.start_pos != bracket_start: 104 return None 105 106 if not nodes: 107 return '' 108 context = module_context.create_context(nodes[0]) 109 return _add_strings(context, nodes, add_slash=True) or '' 110 111 if start_leaf.type == 'error_leaf': 112 # Unfinished string literal, like `join('` 113 value_node = start_leaf.parent 114 index = value_node.children.index(start_leaf) 115 if index > 0: 116 error_node = value_node.children[index - 1] 117 if error_node.type == 'error_node' and len(error_node.children) >= 2: 118 index = -2 119 if error_node.children[-1].type == 'arglist': 120 arglist_nodes = error_node.children[-1].children 121 index -= 1 122 else: 123 arglist_nodes = [] 124 125 return check(error_node.children[index + 1], arglist_nodes[::2]) 126 return None 127 128 # Maybe an arglist or some weird error case. Therefore checked below. 129 searched_node_child = start_leaf 130 while searched_node_child.parent is not None \ 131 and searched_node_child.parent.type not in ('arglist', 'trailer', 'error_node'): 132 searched_node_child = searched_node_child.parent 133 134 if searched_node_child.get_first_leaf() is not start_leaf: 135 return None 136 searched_node = searched_node_child.parent 137 if searched_node is None: 138 return None 139 140 index = searched_node.children.index(searched_node_child) 141 arglist_nodes = searched_node.children[:index] 142 if searched_node.type == 'arglist': 143 trailer = searched_node.parent 144 if trailer.type == 'error_node': 145 trailer_index = trailer.children.index(searched_node) 146 assert trailer_index >= 2 147 assert trailer.children[trailer_index - 1] == '(' 148 return check(trailer.children[trailer_index - 1], arglist_nodes[::2]) 149 elif trailer.type == 'trailer': 150 return check(trailer.children[0], arglist_nodes[::2]) 151 elif searched_node.type == 'trailer': 152 return check(searched_node.children[0], []) 153 elif searched_node.type == 'error_node': 154 # Stuff like `join(""` 155 return check(arglist_nodes[-1], []) 156