1import re 2from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Optional 3 4if TYPE_CHECKING: 5 from .settings import Config 6else: 7 Config = Any 8 9_import_line_intro_re = re.compile("^(?:from|import) ") 10_import_line_midline_import_re = re.compile(" import ") 11 12 13def module_key( 14 module_name: str, 15 config: Config, 16 sub_imports: bool = False, 17 ignore_case: bool = False, 18 section_name: Optional[Any] = None, 19 straight_import: Optional[bool] = False, 20) -> str: 21 match = re.match(r"^(\.+)\s*(.*)", module_name) 22 if match: 23 sep = " " if config.reverse_relative else "_" 24 module_name = sep.join(match.groups()) 25 26 prefix = "" 27 if ignore_case: 28 module_name = str(module_name).lower() 29 else: 30 module_name = str(module_name) 31 32 if sub_imports and config.order_by_type: 33 if module_name in config.constants: 34 prefix = "A" 35 elif module_name in config.classes: 36 prefix = "B" 37 elif module_name in config.variables: 38 prefix = "C" 39 elif module_name.isupper() and len(module_name) > 1: # see issue #376 40 prefix = "A" 41 elif module_name in config.classes or module_name[0:1].isupper(): 42 prefix = "B" 43 else: 44 prefix = "C" 45 if not config.case_sensitive: 46 module_name = module_name.lower() 47 48 length_sort = ( 49 config.length_sort 50 or (config.length_sort_straight and straight_import) 51 or str(section_name).lower() in config.length_sort_sections 52 ) 53 _length_sort_maybe = (str(len(module_name)) + ":" + module_name) if length_sort else module_name 54 return f"{module_name in config.force_to_top and 'A' or 'B'}{prefix}{_length_sort_maybe}" 55 56 57def section_key(line: str, config: Config) -> str: 58 section = "B" 59 60 if ( 61 not config.sort_relative_in_force_sorted_sections 62 and config.reverse_relative 63 and line.startswith("from .") 64 ): 65 match = re.match(r"^from (\.+)\s*(.*)", line) 66 if match: # pragma: no cover - regex always matches if line starts with "from ." 67 line = f"from {' '.join(match.groups())}" 68 if config.group_by_package and line.strip().startswith("from"): 69 line = line.split(" import", 1)[0] 70 71 if config.lexicographical: 72 line = _import_line_intro_re.sub("", _import_line_midline_import_re.sub(".", line)) 73 else: 74 line = re.sub("^from ", "", line) 75 line = re.sub("^import ", "", line) 76 if config.sort_relative_in_force_sorted_sections: 77 sep = " " if config.reverse_relative else "_" 78 line = re.sub(r"^(\.+)", fr"\1{sep}", line) 79 if line.split(" ")[0] in config.force_to_top: 80 section = "A" 81 # * If honor_case_in_force_sorted_sections is true, and case_sensitive and 82 # order_by_type are different, only ignore case in part of the line. 83 # * Otherwise, let order_by_type decide the sorting of the whole line. This 84 # is only "correct" if case_sensitive and order_by_type have the same value. 85 if config.honor_case_in_force_sorted_sections and config.case_sensitive != config.order_by_type: 86 split_module = line.split(" import ", 1) 87 if len(split_module) > 1: 88 module_name, names = split_module 89 if not config.case_sensitive: 90 module_name = module_name.lower() 91 if not config.order_by_type: 92 names = names.lower() 93 line = " import ".join([module_name, names]) 94 elif not config.case_sensitive: 95 line = line.lower() 96 elif not config.order_by_type: 97 line = line.lower() 98 99 return f"{section}{len(line) if config.length_sort else ''}{line}" 100 101 102def sort( 103 config: Config, 104 to_sort: Iterable[str], 105 key: Optional[Callable[[str], Any]] = None, 106 reverse: bool = False, 107) -> List[str]: 108 return config.sorting_function(to_sort, key=key, reverse=reverse) 109 110 111def naturally( 112 to_sort: Iterable[str], key: Optional[Callable[[str], Any]] = None, reverse: bool = False 113) -> List[str]: 114 """Returns a naturally sorted list""" 115 if key is None: 116 key_callback = _natural_keys 117 else: 118 119 def key_callback(text: str) -> List[Any]: 120 return _natural_keys(key(text)) # type: ignore 121 122 return sorted(to_sort, key=key_callback, reverse=reverse) 123 124 125def _atoi(text: str) -> Any: 126 return int(text) if text.isdigit() else text 127 128 129def _natural_keys(text: str) -> List[Any]: 130 return [_atoi(c) for c in re.split(r"(\d+)", text)] 131