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