settings.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937
  1. """isort/settings.py.
  2. Defines how the default settings for isort should be loaded
  3. """
  4. import configparser
  5. import fnmatch
  6. import os
  7. import posixpath
  8. import re
  9. import stat
  10. import subprocess # nosec: Needed for gitignore support.
  11. import sys
  12. from dataclasses import dataclass, field
  13. from functools import lru_cache
  14. from pathlib import Path
  15. from typing import (
  16. TYPE_CHECKING,
  17. Any,
  18. Callable,
  19. Dict,
  20. FrozenSet,
  21. Iterable,
  22. List,
  23. Optional,
  24. Pattern,
  25. Set,
  26. Tuple,
  27. Type,
  28. Union,
  29. )
  30. from warnings import warn
  31. from . import sorting, stdlibs
  32. from .exceptions import (
  33. FormattingPluginDoesNotExist,
  34. InvalidSettingsPath,
  35. ProfileDoesNotExist,
  36. SortingFunctionDoesNotExist,
  37. UnsupportedSettings,
  38. )
  39. from .profiles import profiles as profiles
  40. from .sections import DEFAULT as SECTION_DEFAULTS
  41. from .sections import FIRSTPARTY, FUTURE, LOCALFOLDER, STDLIB, THIRDPARTY
  42. from .utils import Trie
  43. from .wrap_modes import WrapModes
  44. from .wrap_modes import from_string as wrap_mode_from_string
  45. if TYPE_CHECKING:
  46. tomli: Any
  47. else:
  48. from ._vendored import tomli
  49. _SHEBANG_RE = re.compile(rb"^#!.*\bpython[23w]?\b")
  50. CYTHON_EXTENSIONS = frozenset({"pyx", "pxd"})
  51. SUPPORTED_EXTENSIONS = frozenset({"py", "pyi", *CYTHON_EXTENSIONS})
  52. BLOCKED_EXTENSIONS = frozenset({"pex"})
  53. FILE_SKIP_COMMENTS: Tuple[str, ...] = (
  54. "isort:" + "skip_file",
  55. "isort: " + "skip_file",
  56. ) # Concatenated to avoid this file being skipped
  57. MAX_CONFIG_SEARCH_DEPTH: int = 25 # The number of parent directories to for a config file within
  58. STOP_CONFIG_SEARCH_ON_DIRS: Tuple[str, ...] = (".git", ".hg")
  59. VALID_PY_TARGETS: Tuple[str, ...] = tuple(
  60. target.replace("py", "") for target in dir(stdlibs) if not target.startswith("_")
  61. )
  62. CONFIG_SOURCES: Tuple[str, ...] = (
  63. ".isort.cfg",
  64. "pyproject.toml",
  65. "setup.cfg",
  66. "tox.ini",
  67. ".editorconfig",
  68. )
  69. DEFAULT_SKIP: FrozenSet[str] = frozenset(
  70. {
  71. ".venv",
  72. "venv",
  73. ".tox",
  74. ".eggs",
  75. ".git",
  76. ".hg",
  77. ".mypy_cache",
  78. ".nox",
  79. ".svn",
  80. ".bzr",
  81. "_build",
  82. "buck-out",
  83. "build",
  84. "dist",
  85. ".pants.d",
  86. ".direnv",
  87. "node_modules",
  88. "__pypackages__",
  89. }
  90. )
  91. CONFIG_SECTIONS: Dict[str, Tuple[str, ...]] = {
  92. ".isort.cfg": ("settings", "isort"),
  93. "pyproject.toml": ("tool.isort",),
  94. "setup.cfg": ("isort", "tool:isort"),
  95. "tox.ini": ("isort", "tool:isort"),
  96. ".editorconfig": ("*", "*.py", "**.py", "*.{py}"),
  97. }
  98. FALLBACK_CONFIG_SECTIONS: Tuple[str, ...] = ("isort", "tool:isort", "tool.isort")
  99. IMPORT_HEADING_PREFIX = "import_heading_"
  100. IMPORT_FOOTER_PREFIX = "import_footer_"
  101. KNOWN_PREFIX = "known_"
  102. KNOWN_SECTION_MAPPING: Dict[str, str] = {
  103. STDLIB: "STANDARD_LIBRARY",
  104. FUTURE: "FUTURE_LIBRARY",
  105. FIRSTPARTY: "FIRST_PARTY",
  106. THIRDPARTY: "THIRD_PARTY",
  107. LOCALFOLDER: "LOCAL_FOLDER",
  108. }
  109. RUNTIME_SOURCE = "runtime"
  110. DEPRECATED_SETTINGS = ("not_skip", "keep_direct_and_as_imports")
  111. _STR_BOOLEAN_MAPPING = {
  112. "y": True,
  113. "yes": True,
  114. "t": True,
  115. "on": True,
  116. "1": True,
  117. "true": True,
  118. "n": False,
  119. "no": False,
  120. "f": False,
  121. "off": False,
  122. "0": False,
  123. "false": False,
  124. }
  125. @dataclass(frozen=True)
  126. class _Config:
  127. """Defines the data schema and defaults used for isort configuration.
  128. NOTE: known lists, such as known_standard_library, are intentionally not complete as they are
  129. dynamically determined later on.
  130. """
  131. py_version: str = "3"
  132. force_to_top: FrozenSet[str] = frozenset()
  133. skip: FrozenSet[str] = DEFAULT_SKIP
  134. extend_skip: FrozenSet[str] = frozenset()
  135. skip_glob: FrozenSet[str] = frozenset()
  136. extend_skip_glob: FrozenSet[str] = frozenset()
  137. skip_gitignore: bool = False
  138. line_length: int = 79
  139. wrap_length: int = 0
  140. line_ending: str = ""
  141. sections: Tuple[str, ...] = SECTION_DEFAULTS
  142. no_sections: bool = False
  143. known_future_library: FrozenSet[str] = frozenset(("__future__",))
  144. known_third_party: FrozenSet[str] = frozenset()
  145. known_first_party: FrozenSet[str] = frozenset()
  146. known_local_folder: FrozenSet[str] = frozenset()
  147. known_standard_library: FrozenSet[str] = frozenset()
  148. extra_standard_library: FrozenSet[str] = frozenset()
  149. known_other: Dict[str, FrozenSet[str]] = field(default_factory=dict)
  150. multi_line_output: WrapModes = WrapModes.GRID # type: ignore
  151. forced_separate: Tuple[str, ...] = ()
  152. indent: str = " " * 4
  153. comment_prefix: str = " #"
  154. length_sort: bool = False
  155. length_sort_straight: bool = False
  156. length_sort_sections: FrozenSet[str] = frozenset()
  157. add_imports: FrozenSet[str] = frozenset()
  158. remove_imports: FrozenSet[str] = frozenset()
  159. append_only: bool = False
  160. reverse_relative: bool = False
  161. force_single_line: bool = False
  162. single_line_exclusions: Tuple[str, ...] = ()
  163. default_section: str = THIRDPARTY
  164. import_headings: Dict[str, str] = field(default_factory=dict)
  165. import_footers: Dict[str, str] = field(default_factory=dict)
  166. balanced_wrapping: bool = False
  167. use_parentheses: bool = False
  168. order_by_type: bool = True
  169. atomic: bool = False
  170. lines_before_imports: int = -1
  171. lines_after_imports: int = -1
  172. lines_between_sections: int = 1
  173. lines_between_types: int = 0
  174. combine_as_imports: bool = False
  175. combine_star: bool = False
  176. include_trailing_comma: bool = False
  177. from_first: bool = False
  178. verbose: bool = False
  179. quiet: bool = False
  180. force_adds: bool = False
  181. force_alphabetical_sort_within_sections: bool = False
  182. force_alphabetical_sort: bool = False
  183. force_grid_wrap: int = 0
  184. force_sort_within_sections: bool = False
  185. lexicographical: bool = False
  186. group_by_package: bool = False
  187. ignore_whitespace: bool = False
  188. no_lines_before: FrozenSet[str] = frozenset()
  189. no_inline_sort: bool = False
  190. ignore_comments: bool = False
  191. case_sensitive: bool = False
  192. sources: Tuple[Dict[str, Any], ...] = ()
  193. virtual_env: str = ""
  194. conda_env: str = ""
  195. ensure_newline_before_comments: bool = False
  196. directory: str = ""
  197. profile: str = ""
  198. honor_noqa: bool = False
  199. src_paths: Tuple[Path, ...] = ()
  200. old_finders: bool = False
  201. remove_redundant_aliases: bool = False
  202. float_to_top: bool = False
  203. filter_files: bool = False
  204. formatter: str = ""
  205. formatting_function: Optional[Callable[[str, str, object], str]] = None
  206. color_output: bool = False
  207. treat_comments_as_code: FrozenSet[str] = frozenset()
  208. treat_all_comments_as_code: bool = False
  209. supported_extensions: FrozenSet[str] = SUPPORTED_EXTENSIONS
  210. blocked_extensions: FrozenSet[str] = BLOCKED_EXTENSIONS
  211. constants: FrozenSet[str] = frozenset()
  212. classes: FrozenSet[str] = frozenset()
  213. variables: FrozenSet[str] = frozenset()
  214. dedup_headings: bool = False
  215. only_sections: bool = False
  216. only_modified: bool = False
  217. combine_straight_imports: bool = False
  218. auto_identify_namespace_packages: bool = True
  219. namespace_packages: FrozenSet[str] = frozenset()
  220. follow_links: bool = True
  221. indented_import_headings: bool = True
  222. honor_case_in_force_sorted_sections: bool = False
  223. sort_relative_in_force_sorted_sections: bool = False
  224. overwrite_in_place: bool = False
  225. reverse_sort: bool = False
  226. star_first: bool = False
  227. import_dependencies = Dict[str, str]
  228. git_ls_files: Dict[Path, Set[str]] = field(default_factory=dict)
  229. format_error: str = "{error}: {message}"
  230. format_success: str = "{success}: {message}"
  231. sort_order: str = "natural"
  232. sort_reexports: bool = False
  233. split_on_trailing_comma: bool = False
  234. def __post_init__(self) -> None:
  235. py_version = self.py_version
  236. if py_version == "auto": # pragma: no cover
  237. if sys.version_info.major == 2 and sys.version_info.minor <= 6:
  238. py_version = "2"
  239. elif sys.version_info.major == 3 and (
  240. sys.version_info.minor <= 5 or sys.version_info.minor >= 12
  241. ):
  242. py_version = "3"
  243. else:
  244. py_version = f"{sys.version_info.major}{sys.version_info.minor}"
  245. if py_version not in VALID_PY_TARGETS:
  246. raise ValueError(
  247. f"The python version {py_version} is not supported. "
  248. "You can set a python version with the -py or --python-version flag. "
  249. f"The following versions are supported: {VALID_PY_TARGETS}"
  250. )
  251. if py_version != "all":
  252. object.__setattr__(self, "py_version", f"py{py_version}")
  253. if not self.known_standard_library:
  254. object.__setattr__(
  255. self, "known_standard_library", frozenset(getattr(stdlibs, self.py_version).stdlib)
  256. )
  257. if self.multi_line_output == WrapModes.VERTICAL_GRID_GROUPED_NO_COMMA: # type: ignore
  258. vertical_grid_grouped = WrapModes.VERTICAL_GRID_GROUPED # type: ignore
  259. object.__setattr__(self, "multi_line_output", vertical_grid_grouped)
  260. if self.force_alphabetical_sort:
  261. object.__setattr__(self, "force_alphabetical_sort_within_sections", True)
  262. object.__setattr__(self, "no_sections", True)
  263. object.__setattr__(self, "lines_between_types", 1)
  264. object.__setattr__(self, "from_first", True)
  265. if self.wrap_length > self.line_length:
  266. raise ValueError(
  267. "wrap_length must be set lower than or equal to line_length: "
  268. f"{self.wrap_length} > {self.line_length}."
  269. )
  270. def __hash__(self) -> int:
  271. return id(self)
  272. _DEFAULT_SETTINGS = {**vars(_Config()), "source": "defaults"}
  273. class Config(_Config):
  274. def __init__(
  275. self,
  276. settings_file: str = "",
  277. settings_path: str = "",
  278. config: Optional[_Config] = None,
  279. **config_overrides: Any,
  280. ):
  281. self._known_patterns: Optional[List[Tuple[Pattern[str], str]]] = None
  282. self._section_comments: Optional[Tuple[str, ...]] = None
  283. self._section_comments_end: Optional[Tuple[str, ...]] = None
  284. self._skips: Optional[FrozenSet[str]] = None
  285. self._skip_globs: Optional[FrozenSet[str]] = None
  286. self._sorting_function: Optional[Callable[..., List[str]]] = None
  287. if config:
  288. config_vars = vars(config).copy()
  289. config_vars.update(config_overrides)
  290. config_vars["py_version"] = config_vars["py_version"].replace("py", "")
  291. config_vars.pop("_known_patterns")
  292. config_vars.pop("_section_comments")
  293. config_vars.pop("_section_comments_end")
  294. config_vars.pop("_skips")
  295. config_vars.pop("_skip_globs")
  296. config_vars.pop("_sorting_function")
  297. super().__init__(**config_vars)
  298. return
  299. # We can't use self.quiet to conditionally show warnings before super.__init__() is called
  300. # at the end of this method. _Config is also frozen so setting self.quiet isn't possible.
  301. # Therefore we extract quiet early here in a variable and use that in warning conditions.
  302. quiet = config_overrides.get("quiet", False)
  303. sources: List[Dict[str, Any]] = [_DEFAULT_SETTINGS]
  304. config_settings: Dict[str, Any]
  305. project_root: str
  306. if settings_file:
  307. config_settings = _get_config_data(
  308. settings_file,
  309. CONFIG_SECTIONS.get(os.path.basename(settings_file), FALLBACK_CONFIG_SECTIONS),
  310. )
  311. project_root = os.path.dirname(settings_file)
  312. if not config_settings and not quiet:
  313. warn(
  314. f"A custom settings file was specified: {settings_file} but no configuration "
  315. "was found inside. This can happen when [settings] is used as the config "
  316. "header instead of [isort]. "
  317. "See: https://pycqa.github.io/isort/docs/configuration/config_files"
  318. "/#custom_config_files for more information."
  319. )
  320. elif settings_path:
  321. if not os.path.exists(settings_path):
  322. raise InvalidSettingsPath(settings_path)
  323. settings_path = os.path.abspath(settings_path)
  324. project_root, config_settings = _find_config(settings_path)
  325. else:
  326. config_settings = {}
  327. project_root = os.getcwd()
  328. profile_name = config_overrides.get("profile", config_settings.get("profile", ""))
  329. profile: Dict[str, Any] = {}
  330. if profile_name:
  331. if profile_name not in profiles:
  332. import pkg_resources
  333. for plugin in pkg_resources.iter_entry_points("isort.profiles"):
  334. profiles.setdefault(plugin.name, plugin.load())
  335. if profile_name not in profiles:
  336. raise ProfileDoesNotExist(profile_name)
  337. profile = profiles[profile_name].copy()
  338. profile["source"] = f"{profile_name} profile"
  339. sources.append(profile)
  340. if config_settings:
  341. sources.append(config_settings)
  342. if config_overrides:
  343. config_overrides["source"] = RUNTIME_SOURCE
  344. sources.append(config_overrides)
  345. combined_config = {**profile, **config_settings, **config_overrides}
  346. if "indent" in combined_config:
  347. indent = str(combined_config["indent"])
  348. if indent.isdigit():
  349. indent = " " * int(indent)
  350. else:
  351. indent = indent.strip("'").strip('"')
  352. if indent.lower() == "tab":
  353. indent = "\t"
  354. combined_config["indent"] = indent
  355. known_other = {}
  356. import_headings = {}
  357. import_footers = {}
  358. for key, value in tuple(combined_config.items()):
  359. # Collect all known sections beyond those that have direct entries
  360. if key.startswith(KNOWN_PREFIX) and key not in (
  361. "known_standard_library",
  362. "known_future_library",
  363. "known_third_party",
  364. "known_first_party",
  365. "known_local_folder",
  366. ):
  367. import_heading = key[len(KNOWN_PREFIX) :].lower()
  368. maps_to_section = import_heading.upper()
  369. combined_config.pop(key)
  370. if maps_to_section in KNOWN_SECTION_MAPPING:
  371. section_name = f"known_{KNOWN_SECTION_MAPPING[maps_to_section].lower()}"
  372. if section_name in combined_config and not quiet:
  373. warn(
  374. f"Can't set both {key} and {section_name} in the same config file.\n"
  375. f"Default to {section_name} if unsure."
  376. "\n\n"
  377. "See: https://pycqa.github.io/isort/"
  378. "#custom-sections-and-ordering."
  379. )
  380. else:
  381. combined_config[section_name] = frozenset(value)
  382. else:
  383. known_other[import_heading] = frozenset(value)
  384. if maps_to_section not in combined_config.get("sections", ()) and not quiet:
  385. warn(
  386. f"`{key}` setting is defined, but {maps_to_section} is not"
  387. " included in `sections` config option:"
  388. f" {combined_config.get('sections', SECTION_DEFAULTS)}.\n\n"
  389. "See: https://pycqa.github.io/isort/"
  390. "#custom-sections-and-ordering."
  391. )
  392. if key.startswith(IMPORT_HEADING_PREFIX):
  393. import_headings[key[len(IMPORT_HEADING_PREFIX) :].lower()] = str(value)
  394. if key.startswith(IMPORT_FOOTER_PREFIX):
  395. import_footers[key[len(IMPORT_FOOTER_PREFIX) :].lower()] = str(value)
  396. # Coerce all provided config values into their correct type
  397. default_value = _DEFAULT_SETTINGS.get(key, None)
  398. if default_value is None:
  399. continue
  400. combined_config[key] = type(default_value)(value)
  401. for section in combined_config.get("sections", ()):
  402. if section in SECTION_DEFAULTS:
  403. continue
  404. if not section.lower() in known_other:
  405. config_keys = ", ".join(known_other.keys())
  406. warn(
  407. f"`sections` setting includes {section}, but no known_{section.lower()} "
  408. "is defined. "
  409. f"The following known_SECTION config options are defined: {config_keys}."
  410. )
  411. if "directory" not in combined_config:
  412. combined_config["directory"] = (
  413. os.path.dirname(config_settings["source"])
  414. if config_settings.get("source", None)
  415. else os.getcwd()
  416. )
  417. path_root = Path(combined_config.get("directory", project_root)).resolve()
  418. path_root = path_root if path_root.is_dir() else path_root.parent
  419. if "src_paths" not in combined_config:
  420. combined_config["src_paths"] = (path_root / "src", path_root)
  421. else:
  422. src_paths: List[Path] = []
  423. for src_path in combined_config.get("src_paths", ()):
  424. full_paths = (
  425. path_root.glob(src_path) if "*" in str(src_path) else [path_root / src_path]
  426. )
  427. for path in full_paths:
  428. if path not in src_paths:
  429. src_paths.append(path)
  430. combined_config["src_paths"] = tuple(src_paths)
  431. if "formatter" in combined_config:
  432. import pkg_resources
  433. for plugin in pkg_resources.iter_entry_points("isort.formatters"):
  434. if plugin.name == combined_config["formatter"]:
  435. combined_config["formatting_function"] = plugin.load()
  436. break
  437. else:
  438. raise FormattingPluginDoesNotExist(combined_config["formatter"])
  439. # Remove any config values that are used for creating config object but
  440. # aren't defined in dataclass
  441. combined_config.pop("source", None)
  442. combined_config.pop("sources", None)
  443. combined_config.pop("runtime_src_paths", None)
  444. deprecated_options_used = [
  445. option for option in combined_config if option in DEPRECATED_SETTINGS
  446. ]
  447. if deprecated_options_used:
  448. for deprecated_option in deprecated_options_used:
  449. combined_config.pop(deprecated_option)
  450. if not quiet:
  451. warn(
  452. "W0503: Deprecated config options were used: "
  453. f"{', '.join(deprecated_options_used)}."
  454. "Please see the 5.0.0 upgrade guide: "
  455. "https://pycqa.github.io/isort/docs/upgrade_guides/5.0.0.html"
  456. )
  457. if known_other:
  458. combined_config["known_other"] = known_other
  459. if import_headings:
  460. for import_heading_key in import_headings:
  461. combined_config.pop(f"{IMPORT_HEADING_PREFIX}{import_heading_key}")
  462. combined_config["import_headings"] = import_headings
  463. if import_footers:
  464. for import_footer_key in import_footers:
  465. combined_config.pop(f"{IMPORT_FOOTER_PREFIX}{import_footer_key}")
  466. combined_config["import_footers"] = import_footers
  467. unsupported_config_errors = {}
  468. for option in set(combined_config.keys()).difference(
  469. getattr(_Config, "__dataclass_fields__", {}).keys()
  470. ):
  471. for source in reversed(sources):
  472. if option in source:
  473. unsupported_config_errors[option] = {
  474. "value": source[option],
  475. "source": source["source"],
  476. }
  477. if unsupported_config_errors:
  478. raise UnsupportedSettings(unsupported_config_errors)
  479. super().__init__(sources=tuple(sources), **combined_config)
  480. def is_supported_filetype(self, file_name: str) -> bool:
  481. _root, ext = os.path.splitext(file_name)
  482. ext = ext.lstrip(".")
  483. if ext in self.supported_extensions:
  484. return True
  485. if ext in self.blocked_extensions:
  486. return False
  487. # Skip editor backup files.
  488. if file_name.endswith("~"):
  489. return False
  490. try:
  491. if stat.S_ISFIFO(os.stat(file_name).st_mode):
  492. return False
  493. except OSError:
  494. pass
  495. try:
  496. with open(file_name, "rb") as fp:
  497. line = fp.readline(100)
  498. except OSError:
  499. return False
  500. else:
  501. return bool(_SHEBANG_RE.match(line))
  502. def _check_folder_git_ls_files(self, folder: str) -> Optional[Path]:
  503. env = {**os.environ, "LANG": "C.UTF-8"}
  504. try:
  505. topfolder_result = subprocess.check_output( # nosec # skipcq: PYL-W1510
  506. ["git", "-C", folder, "rev-parse", "--show-toplevel"], encoding="utf-8", env=env
  507. )
  508. except subprocess.CalledProcessError:
  509. return None
  510. git_folder = Path(topfolder_result.rstrip()).resolve()
  511. # files committed to git
  512. tracked_files = (
  513. subprocess.check_output( # nosec # skipcq: PYL-W1510
  514. ["git", "-C", str(git_folder), "ls-files", "-z"],
  515. encoding="utf-8",
  516. env=env,
  517. )
  518. .rstrip("\0")
  519. .split("\0")
  520. )
  521. # files that haven't been committed yet, but aren't ignored
  522. tracked_files_others = (
  523. subprocess.check_output( # nosec # skipcq: PYL-W1510
  524. ["git", "-C", str(git_folder), "ls-files", "-z", "--others", "--exclude-standard"],
  525. encoding="utf-8",
  526. env=env,
  527. )
  528. .rstrip("\0")
  529. .split("\0")
  530. )
  531. self.git_ls_files[git_folder] = {
  532. str(git_folder / Path(f)) for f in tracked_files + tracked_files_others
  533. }
  534. return git_folder
  535. def is_skipped(self, file_path: Path) -> bool:
  536. """Returns True if the file and/or folder should be skipped based on current settings."""
  537. if self.directory and Path(self.directory) in file_path.resolve().parents:
  538. file_name = os.path.relpath(file_path.resolve(), self.directory)
  539. else:
  540. file_name = str(file_path)
  541. os_path = str(file_path)
  542. normalized_path = os_path.replace("\\", "/")
  543. if normalized_path[1:2] == ":":
  544. normalized_path = normalized_path[2:]
  545. for skip_path in self.skips:
  546. if posixpath.abspath(normalized_path) == posixpath.abspath(
  547. skip_path.replace("\\", "/")
  548. ):
  549. return True
  550. position = os.path.split(file_name)
  551. while position[1]:
  552. if position[1] in self.skips:
  553. return True
  554. position = os.path.split(position[0])
  555. for sglob in self.skip_globs:
  556. if fnmatch.fnmatch(file_name, sglob) or fnmatch.fnmatch("/" + file_name, sglob):
  557. return True
  558. if not (os.path.isfile(os_path) or os.path.isdir(os_path) or os.path.islink(os_path)):
  559. return True
  560. if self.skip_gitignore:
  561. if file_path.name == ".git": # pragma: no cover
  562. return True
  563. git_folder = None
  564. file_paths = [file_path, file_path.resolve()]
  565. for folder in self.git_ls_files:
  566. if any(folder in path.parents for path in file_paths):
  567. git_folder = folder
  568. break
  569. else:
  570. git_folder = self._check_folder_git_ls_files(str(file_path.parent))
  571. # git_ls_files are good files you should parse. If you're not in the allow list, skip.
  572. if (
  573. git_folder
  574. and not file_path.is_dir()
  575. and str(file_path.resolve()) not in self.git_ls_files[git_folder]
  576. ):
  577. return True
  578. return False
  579. @property
  580. def known_patterns(self) -> List[Tuple[Pattern[str], str]]:
  581. if self._known_patterns is not None:
  582. return self._known_patterns
  583. self._known_patterns = []
  584. pattern_sections = [STDLIB] + [section for section in self.sections if section != STDLIB]
  585. for placement in reversed(pattern_sections):
  586. known_placement = KNOWN_SECTION_MAPPING.get(placement, placement).lower()
  587. config_key = f"{KNOWN_PREFIX}{known_placement}"
  588. known_modules = getattr(self, config_key, self.known_other.get(known_placement, ()))
  589. extra_modules = getattr(self, f"extra_{known_placement}", ())
  590. all_modules = set(extra_modules).union(known_modules)
  591. known_patterns = [
  592. pattern
  593. for known_pattern in all_modules
  594. for pattern in self._parse_known_pattern(known_pattern)
  595. ]
  596. for known_pattern in known_patterns:
  597. regexp = "^" + known_pattern.replace("*", ".*").replace("?", ".?") + "$"
  598. self._known_patterns.append((re.compile(regexp), placement))
  599. return self._known_patterns
  600. @property
  601. def section_comments(self) -> Tuple[str, ...]:
  602. if self._section_comments is not None:
  603. return self._section_comments
  604. self._section_comments = tuple(f"# {heading}" for heading in self.import_headings.values())
  605. return self._section_comments
  606. @property
  607. def section_comments_end(self) -> Tuple[str, ...]:
  608. if self._section_comments_end is not None:
  609. return self._section_comments_end
  610. self._section_comments_end = tuple(f"# {footer}" for footer in self.import_footers.values())
  611. return self._section_comments_end
  612. @property
  613. def skips(self) -> FrozenSet[str]:
  614. if self._skips is not None:
  615. return self._skips
  616. self._skips = self.skip.union(self.extend_skip)
  617. return self._skips
  618. @property
  619. def skip_globs(self) -> FrozenSet[str]:
  620. if self._skip_globs is not None:
  621. return self._skip_globs
  622. self._skip_globs = self.skip_glob.union(self.extend_skip_glob)
  623. return self._skip_globs
  624. @property
  625. def sorting_function(self) -> Callable[..., List[str]]:
  626. if self._sorting_function is not None:
  627. return self._sorting_function
  628. if self.sort_order == "natural":
  629. self._sorting_function = sorting.naturally
  630. elif self.sort_order == "native":
  631. self._sorting_function = sorted
  632. else:
  633. available_sort_orders = ["natural", "native"]
  634. import pkg_resources
  635. for sort_plugin in pkg_resources.iter_entry_points("isort.sort_function"):
  636. available_sort_orders.append(sort_plugin.name)
  637. if sort_plugin.name == self.sort_order:
  638. self._sorting_function = sort_plugin.load()
  639. break
  640. else:
  641. raise SortingFunctionDoesNotExist(self.sort_order, available_sort_orders)
  642. return self._sorting_function
  643. def _parse_known_pattern(self, pattern: str) -> List[str]:
  644. """Expand pattern if identified as a directory and return found sub packages"""
  645. if pattern.endswith(os.path.sep):
  646. patterns = [
  647. filename
  648. for filename in os.listdir(os.path.join(self.directory, pattern))
  649. if os.path.isdir(os.path.join(self.directory, pattern, filename))
  650. ]
  651. else:
  652. patterns = [pattern]
  653. return patterns
  654. def _get_str_to_type_converter(setting_name: str) -> Union[Callable[[str], Any], Type[Any]]:
  655. type_converter: Union[Callable[[str], Any], Type[Any]] = type(
  656. _DEFAULT_SETTINGS.get(setting_name, "")
  657. )
  658. if type_converter == WrapModes:
  659. type_converter = wrap_mode_from_string
  660. return type_converter
  661. def _as_list(value: str) -> List[str]:
  662. if isinstance(value, list):
  663. return [item.strip() for item in value]
  664. filtered = [item.strip() for item in value.replace("\n", ",").split(",") if item.strip()]
  665. return filtered
  666. def _abspaths(cwd: str, values: Iterable[str]) -> Set[str]:
  667. paths = {
  668. os.path.join(cwd, value)
  669. if not value.startswith(os.path.sep) and value.endswith(os.path.sep)
  670. else value
  671. for value in values
  672. }
  673. return paths
  674. @lru_cache()
  675. def _find_config(path: str) -> Tuple[str, Dict[str, Any]]:
  676. current_directory = path
  677. tries = 0
  678. while current_directory and tries < MAX_CONFIG_SEARCH_DEPTH:
  679. for config_file_name in CONFIG_SOURCES:
  680. potential_config_file = os.path.join(current_directory, config_file_name)
  681. if os.path.isfile(potential_config_file):
  682. config_data: Dict[str, Any]
  683. try:
  684. config_data = _get_config_data(
  685. potential_config_file, CONFIG_SECTIONS[config_file_name]
  686. )
  687. except Exception:
  688. warn(f"Failed to pull configuration information from {potential_config_file}")
  689. config_data = {}
  690. if config_data:
  691. return (current_directory, config_data)
  692. for stop_dir in STOP_CONFIG_SEARCH_ON_DIRS:
  693. if os.path.isdir(os.path.join(current_directory, stop_dir)):
  694. return (current_directory, {})
  695. new_directory = os.path.split(current_directory)[0]
  696. if new_directory == current_directory:
  697. break
  698. current_directory = new_directory
  699. tries += 1
  700. return (path, {})
  701. @lru_cache()
  702. def find_all_configs(path: str) -> Trie:
  703. """
  704. Looks for config files in the path provided and in all of its sub-directories.
  705. Parses and stores any config file encountered in a trie and returns the root of
  706. the trie
  707. """
  708. trie_root = Trie("default", {})
  709. for (dirpath, _, _) in os.walk(path):
  710. for config_file_name in CONFIG_SOURCES:
  711. potential_config_file = os.path.join(dirpath, config_file_name)
  712. if os.path.isfile(potential_config_file):
  713. config_data: Dict[str, Any]
  714. try:
  715. config_data = _get_config_data(
  716. potential_config_file, CONFIG_SECTIONS[config_file_name]
  717. )
  718. except Exception:
  719. warn(f"Failed to pull configuration information from {potential_config_file}")
  720. config_data = {}
  721. if config_data:
  722. trie_root.insert(potential_config_file, config_data)
  723. break
  724. return trie_root
  725. @lru_cache()
  726. def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]:
  727. settings: Dict[str, Any] = {}
  728. if file_path.endswith(".toml"):
  729. with open(file_path, "rb") as bin_config_file:
  730. config = tomli.load(bin_config_file)
  731. for section in sections:
  732. config_section = config
  733. for key in section.split("."):
  734. config_section = config_section.get(key, {})
  735. settings.update(config_section)
  736. else:
  737. with open(file_path, encoding="utf-8") as config_file:
  738. if file_path.endswith(".editorconfig"):
  739. line = "\n"
  740. last_position = config_file.tell()
  741. while line:
  742. line = config_file.readline()
  743. if "[" in line:
  744. config_file.seek(last_position)
  745. break
  746. last_position = config_file.tell()
  747. config = configparser.ConfigParser(strict=False)
  748. config.read_file(config_file)
  749. for section in sections:
  750. if section.startswith("*.{") and section.endswith("}"):
  751. extension = section[len("*.{") : -1]
  752. for config_key in config.keys():
  753. if (
  754. config_key.startswith("*.{")
  755. and config_key.endswith("}")
  756. and extension
  757. in map(
  758. lambda text: text.strip(), config_key[len("*.{") : -1].split(",") # type: ignore # noqa
  759. )
  760. ):
  761. settings.update(config.items(config_key))
  762. elif config.has_section(section):
  763. settings.update(config.items(section))
  764. if settings:
  765. settings["source"] = file_path
  766. if file_path.endswith(".editorconfig"):
  767. indent_style = settings.pop("indent_style", "").strip()
  768. indent_size = settings.pop("indent_size", "").strip()
  769. if indent_size == "tab":
  770. indent_size = settings.pop("tab_width", "").strip()
  771. if indent_style == "space":
  772. settings["indent"] = " " * (indent_size and int(indent_size) or 4)
  773. elif indent_style == "tab":
  774. settings["indent"] = "\t" * (indent_size and int(indent_size) or 1)
  775. max_line_length = settings.pop("max_line_length", "").strip()
  776. if max_line_length and (max_line_length == "off" or max_line_length.isdigit()):
  777. settings["line_length"] = (
  778. float("inf") if max_line_length == "off" else int(max_line_length)
  779. )
  780. settings = {
  781. key: value
  782. for key, value in settings.items()
  783. if key in _DEFAULT_SETTINGS.keys() or key.startswith(KNOWN_PREFIX)
  784. }
  785. for key, value in settings.items():
  786. existing_value_type = _get_str_to_type_converter(key)
  787. if existing_value_type == tuple:
  788. settings[key] = tuple(_as_list(value))
  789. elif existing_value_type == frozenset:
  790. settings[key] = frozenset(_as_list(settings.get(key))) # type: ignore
  791. elif existing_value_type == bool:
  792. # Only some configuration formats support native boolean values.
  793. if not isinstance(value, bool):
  794. value = _as_bool(value)
  795. settings[key] = value
  796. elif key.startswith(KNOWN_PREFIX):
  797. settings[key] = _abspaths(os.path.dirname(file_path), _as_list(value))
  798. elif key == "force_grid_wrap":
  799. try:
  800. result = existing_value_type(value)
  801. except ValueError: # backwards compatibility for true / false force grid wrap
  802. result = 0 if value.lower().strip() == "false" else 2
  803. settings[key] = result
  804. elif key == "comment_prefix":
  805. settings[key] = str(value).strip("'").strip('"')
  806. else:
  807. settings[key] = existing_value_type(value)
  808. return settings
  809. def _as_bool(value: str) -> bool:
  810. """Given a string value that represents True or False, returns the Boolean equivalent.
  811. Heavily inspired from distutils strtobool.
  812. """
  813. try:
  814. return _STR_BOOLEAN_MAPPING[value.lower()]
  815. except KeyError:
  816. raise ValueError(f"invalid truth value {value}")
  817. DEFAULT_CONFIG = Config()