settings.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925
  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 functools import lru_cache
  13. from pathlib import Path
  14. from typing import (
  15. TYPE_CHECKING,
  16. Any,
  17. Callable,
  18. Dict,
  19. FrozenSet,
  20. Iterable,
  21. List,
  22. Optional,
  23. Pattern,
  24. Set,
  25. Tuple,
  26. Type,
  27. Union,
  28. )
  29. from warnings import warn
  30. from . import sorting, stdlibs
  31. from ._future import dataclass, field
  32. from .exceptions import (
  33. FormattingPluginDoesNotExist,
  34. InvalidSettingsPath,
  35. ProfileDoesNotExist,
  36. SortingFunctionDoesNotExist,
  37. UnsupportedSettings,
  38. )
  39. from .profiles import 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(br"^#!.*\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_ignore: Dict[Path, Set[Path]] = field(default_factory=dict)
  229. format_error: str = "{error}: {message}"
  230. format_success: str = "{success}: {message}"
  231. sort_order: str = "natural"
  232. def __post_init__(self) -> None:
  233. py_version = self.py_version
  234. if py_version == "auto": # pragma: no cover
  235. if sys.version_info.major == 2 and sys.version_info.minor <= 6:
  236. py_version = "2"
  237. elif sys.version_info.major == 3 and (
  238. sys.version_info.minor <= 5 or sys.version_info.minor >= 10
  239. ):
  240. py_version = "3"
  241. else:
  242. py_version = f"{sys.version_info.major}{sys.version_info.minor}"
  243. if py_version not in VALID_PY_TARGETS:
  244. raise ValueError(
  245. f"The python version {py_version} is not supported. "
  246. "You can set a python version with the -py or --python-version flag. "
  247. f"The following versions are supported: {VALID_PY_TARGETS}"
  248. )
  249. if py_version != "all":
  250. object.__setattr__(self, "py_version", f"py{py_version}")
  251. if not self.known_standard_library:
  252. object.__setattr__(
  253. self, "known_standard_library", frozenset(getattr(stdlibs, self.py_version).stdlib)
  254. )
  255. if self.multi_line_output == WrapModes.VERTICAL_GRID_GROUPED_NO_COMMA: # type: ignore
  256. vertical_grid_grouped = WrapModes.VERTICAL_GRID_GROUPED # type: ignore
  257. object.__setattr__(self, "multi_line_output", vertical_grid_grouped)
  258. if self.force_alphabetical_sort:
  259. object.__setattr__(self, "force_alphabetical_sort_within_sections", True)
  260. object.__setattr__(self, "no_sections", True)
  261. object.__setattr__(self, "lines_between_types", 1)
  262. object.__setattr__(self, "from_first", True)
  263. if self.wrap_length > self.line_length:
  264. raise ValueError(
  265. "wrap_length must be set lower than or equal to line_length: "
  266. f"{self.wrap_length} > {self.line_length}."
  267. )
  268. def __hash__(self) -> int:
  269. return id(self)
  270. _DEFAULT_SETTINGS = {**vars(_Config()), "source": "defaults"}
  271. class Config(_Config):
  272. def __init__(
  273. self,
  274. settings_file: str = "",
  275. settings_path: str = "",
  276. config: Optional[_Config] = None,
  277. **config_overrides: Any,
  278. ):
  279. self._known_patterns: Optional[List[Tuple[Pattern[str], str]]] = None
  280. self._section_comments: Optional[Tuple[str, ...]] = None
  281. self._section_comments_end: Optional[Tuple[str, ...]] = None
  282. self._skips: Optional[FrozenSet[str]] = None
  283. self._skip_globs: Optional[FrozenSet[str]] = None
  284. self._sorting_function: Optional[Callable[..., List[str]]] = None
  285. if config:
  286. config_vars = vars(config).copy()
  287. config_vars.update(config_overrides)
  288. config_vars["py_version"] = config_vars["py_version"].replace("py", "")
  289. config_vars.pop("_known_patterns")
  290. config_vars.pop("_section_comments")
  291. config_vars.pop("_section_comments_end")
  292. config_vars.pop("_skips")
  293. config_vars.pop("_skip_globs")
  294. config_vars.pop("_sorting_function")
  295. super().__init__(**config_vars) # type: ignore
  296. return
  297. # We can't use self.quiet to conditionally show warnings before super.__init__() is called
  298. # at the end of this method. _Config is also frozen so setting self.quiet isn't possible.
  299. # Therefore we extract quiet early here in a variable and use that in warning conditions.
  300. quiet = config_overrides.get("quiet", False)
  301. sources: List[Dict[str, Any]] = [_DEFAULT_SETTINGS]
  302. config_settings: Dict[str, Any]
  303. project_root: str
  304. if settings_file:
  305. config_settings = _get_config_data(
  306. settings_file,
  307. CONFIG_SECTIONS.get(os.path.basename(settings_file), FALLBACK_CONFIG_SECTIONS),
  308. )
  309. project_root = os.path.dirname(settings_file)
  310. if not config_settings and not quiet:
  311. warn(
  312. f"A custom settings file was specified: {settings_file} but no configuration "
  313. "was found inside. This can happen when [settings] is used as the config "
  314. "header instead of [isort]. "
  315. "See: https://pycqa.github.io/isort/docs/configuration/config_files"
  316. "/#custom_config_files for more information."
  317. )
  318. elif settings_path:
  319. if not os.path.exists(settings_path):
  320. raise InvalidSettingsPath(settings_path)
  321. settings_path = os.path.abspath(settings_path)
  322. project_root, config_settings = _find_config(settings_path)
  323. else:
  324. config_settings = {}
  325. project_root = os.getcwd()
  326. profile_name = config_overrides.get("profile", config_settings.get("profile", ""))
  327. profile: Dict[str, Any] = {}
  328. if profile_name:
  329. if profile_name not in profiles:
  330. import pkg_resources
  331. for plugin in pkg_resources.iter_entry_points("isort.profiles"):
  332. profiles.setdefault(plugin.name, plugin.load())
  333. if profile_name not in profiles:
  334. raise ProfileDoesNotExist(profile_name)
  335. profile = profiles[profile_name].copy()
  336. profile["source"] = f"{profile_name} profile"
  337. sources.append(profile)
  338. if config_settings:
  339. sources.append(config_settings)
  340. if config_overrides:
  341. config_overrides["source"] = RUNTIME_SOURCE
  342. sources.append(config_overrides)
  343. combined_config = {**profile, **config_settings, **config_overrides}
  344. if "indent" in combined_config:
  345. indent = str(combined_config["indent"])
  346. if indent.isdigit():
  347. indent = " " * int(indent)
  348. else:
  349. indent = indent.strip("'").strip('"')
  350. if indent.lower() == "tab":
  351. indent = "\t"
  352. combined_config["indent"] = indent
  353. known_other = {}
  354. import_headings = {}
  355. import_footers = {}
  356. for key, value in tuple(combined_config.items()):
  357. # Collect all known sections beyond those that have direct entries
  358. if key.startswith(KNOWN_PREFIX) and key not in (
  359. "known_standard_library",
  360. "known_future_library",
  361. "known_third_party",
  362. "known_first_party",
  363. "known_local_folder",
  364. ):
  365. import_heading = key[len(KNOWN_PREFIX) :].lower()
  366. maps_to_section = import_heading.upper()
  367. combined_config.pop(key)
  368. if maps_to_section in KNOWN_SECTION_MAPPING:
  369. section_name = f"known_{KNOWN_SECTION_MAPPING[maps_to_section].lower()}"
  370. if section_name in combined_config and not quiet:
  371. warn(
  372. f"Can't set both {key} and {section_name} in the same config file.\n"
  373. f"Default to {section_name} if unsure."
  374. "\n\n"
  375. "See: https://pycqa.github.io/isort/"
  376. "#custom-sections-and-ordering."
  377. )
  378. else:
  379. combined_config[section_name] = frozenset(value)
  380. else:
  381. known_other[import_heading] = frozenset(value)
  382. if maps_to_section not in combined_config.get("sections", ()) and not quiet:
  383. warn(
  384. f"`{key}` setting is defined, but {maps_to_section} is not"
  385. " included in `sections` config option:"
  386. f" {combined_config.get('sections', SECTION_DEFAULTS)}.\n\n"
  387. "See: https://pycqa.github.io/isort/"
  388. "#custom-sections-and-ordering."
  389. )
  390. if key.startswith(IMPORT_HEADING_PREFIX):
  391. import_headings[key[len(IMPORT_HEADING_PREFIX) :].lower()] = str(value)
  392. if key.startswith(IMPORT_FOOTER_PREFIX):
  393. import_footers[key[len(IMPORT_FOOTER_PREFIX) :].lower()] = str(value)
  394. # Coerce all provided config values into their correct type
  395. default_value = _DEFAULT_SETTINGS.get(key, None)
  396. if default_value is None:
  397. continue
  398. combined_config[key] = type(default_value)(value)
  399. for section in combined_config.get("sections", ()):
  400. if section in SECTION_DEFAULTS:
  401. continue
  402. if not section.lower() in known_other:
  403. config_keys = ", ".join(known_other.keys())
  404. warn(
  405. f"`sections` setting includes {section}, but no known_{section.lower()} "
  406. "is defined. "
  407. f"The following known_SECTION config options are defined: {config_keys}."
  408. )
  409. if "directory" not in combined_config:
  410. combined_config["directory"] = (
  411. os.path.dirname(config_settings["source"])
  412. if config_settings.get("source", None)
  413. else os.getcwd()
  414. )
  415. path_root = Path(combined_config.get("directory", project_root)).resolve()
  416. path_root = path_root if path_root.is_dir() else path_root.parent
  417. if "src_paths" not in combined_config:
  418. combined_config["src_paths"] = (path_root / "src", path_root)
  419. else:
  420. src_paths: List[Path] = []
  421. for src_path in combined_config.get("src_paths", ()):
  422. full_paths = (
  423. path_root.glob(src_path) if "*" in str(src_path) else [path_root / src_path]
  424. )
  425. for path in full_paths:
  426. if path not in src_paths:
  427. src_paths.append(path)
  428. combined_config["src_paths"] = tuple(src_paths)
  429. if "formatter" in combined_config:
  430. import pkg_resources
  431. for plugin in pkg_resources.iter_entry_points("isort.formatters"):
  432. if plugin.name == combined_config["formatter"]:
  433. combined_config["formatting_function"] = plugin.load()
  434. break
  435. else:
  436. raise FormattingPluginDoesNotExist(combined_config["formatter"])
  437. # Remove any config values that are used for creating config object but
  438. # aren't defined in dataclass
  439. combined_config.pop("source", None)
  440. combined_config.pop("sources", None)
  441. combined_config.pop("runtime_src_paths", None)
  442. deprecated_options_used = [
  443. option for option in combined_config if option in DEPRECATED_SETTINGS
  444. ]
  445. if deprecated_options_used:
  446. for deprecated_option in deprecated_options_used:
  447. combined_config.pop(deprecated_option)
  448. if not quiet:
  449. warn(
  450. "W0503: Deprecated config options were used: "
  451. f"{', '.join(deprecated_options_used)}."
  452. "Please see the 5.0.0 upgrade guide: "
  453. "https://pycqa.github.io/isort/docs/upgrade_guides/5.0.0.html"
  454. )
  455. if known_other:
  456. combined_config["known_other"] = known_other
  457. if import_headings:
  458. for import_heading_key in import_headings:
  459. combined_config.pop(f"{IMPORT_HEADING_PREFIX}{import_heading_key}")
  460. combined_config["import_headings"] = import_headings
  461. if import_footers:
  462. for import_footer_key in import_footers:
  463. combined_config.pop(f"{IMPORT_FOOTER_PREFIX}{import_footer_key}")
  464. combined_config["import_footers"] = import_footers
  465. unsupported_config_errors = {}
  466. for option in set(combined_config.keys()).difference(
  467. getattr(_Config, "__dataclass_fields__", {}).keys()
  468. ):
  469. for source in reversed(sources):
  470. if option in source:
  471. unsupported_config_errors[option] = {
  472. "value": source[option],
  473. "source": source["source"],
  474. }
  475. if unsupported_config_errors:
  476. raise UnsupportedSettings(unsupported_config_errors)
  477. super().__init__(sources=tuple(sources), **combined_config) # type: ignore
  478. def is_supported_filetype(self, file_name: str) -> bool:
  479. _root, ext = os.path.splitext(file_name)
  480. ext = ext.lstrip(".")
  481. if ext in self.supported_extensions:
  482. return True
  483. if ext in self.blocked_extensions:
  484. return False
  485. # Skip editor backup files.
  486. if file_name.endswith("~"):
  487. return False
  488. try:
  489. if stat.S_ISFIFO(os.stat(file_name).st_mode):
  490. return False
  491. except OSError:
  492. pass
  493. try:
  494. with open(file_name, "rb") as fp:
  495. line = fp.readline(100)
  496. except OSError:
  497. return False
  498. else:
  499. return bool(_SHEBANG_RE.match(line))
  500. def _check_folder_gitignore(self, folder: str) -> Optional[Path]:
  501. env = {**os.environ, "LANG": "C.UTF-8"}
  502. try:
  503. topfolder_result = subprocess.check_output( # nosec # skipcq: PYL-W1510
  504. ["git", "-C", folder, "rev-parse", "--show-toplevel"], encoding="utf-8", env=env
  505. )
  506. except subprocess.CalledProcessError:
  507. return None
  508. git_folder = Path(topfolder_result.rstrip()).resolve()
  509. files: List[str] = []
  510. # don't check symlinks; either part of the repo and would be checked
  511. # twice, or is external to the repo and git won't know anything about it
  512. for root, _dirs, git_files in os.walk(git_folder, followlinks=False):
  513. if ".git" in _dirs:
  514. _dirs.remove(".git")
  515. for git_file in git_files:
  516. files.append(os.path.join(root, git_file))
  517. git_options = ["-C", str(git_folder), "-c", "core.quotePath="]
  518. try:
  519. ignored = subprocess.check_output( # nosec # skipcq: PYL-W1510
  520. ["git", *git_options, "check-ignore", "-z", "--stdin", "--no-index"],
  521. encoding="utf-8",
  522. env=env,
  523. input="\0".join(files),
  524. )
  525. except subprocess.CalledProcessError:
  526. return None
  527. self.git_ignore[git_folder] = {Path(f) for f in ignored.rstrip("\0").split("\0")}
  528. return git_folder
  529. def is_skipped(self, file_path: Path) -> bool:
  530. """Returns True if the file and/or folder should be skipped based on current settings."""
  531. if self.directory and Path(self.directory) in file_path.resolve().parents:
  532. file_name = os.path.relpath(file_path.resolve(), self.directory)
  533. else:
  534. file_name = str(file_path)
  535. os_path = str(file_path)
  536. normalized_path = os_path.replace("\\", "/")
  537. if normalized_path[1:2] == ":":
  538. normalized_path = normalized_path[2:]
  539. for skip_path in self.skips:
  540. if posixpath.abspath(normalized_path) == posixpath.abspath(
  541. skip_path.replace("\\", "/")
  542. ):
  543. return True
  544. position = os.path.split(file_name)
  545. while position[1]:
  546. if position[1] in self.skips:
  547. return True
  548. position = os.path.split(position[0])
  549. for sglob in self.skip_globs:
  550. if fnmatch.fnmatch(file_name, sglob) or fnmatch.fnmatch("/" + file_name, sglob):
  551. return True
  552. if not (os.path.isfile(os_path) or os.path.isdir(os_path) or os.path.islink(os_path)):
  553. return True
  554. if self.skip_gitignore:
  555. if file_path.name == ".git": # pragma: no cover
  556. return True
  557. git_folder = None
  558. file_paths = [file_path, file_path.resolve()]
  559. for folder in self.git_ignore:
  560. if any(folder in path.parents for path in file_paths):
  561. git_folder = folder
  562. break
  563. else:
  564. git_folder = self._check_folder_gitignore(str(file_path.parent))
  565. if git_folder and any(path in self.git_ignore[git_folder] for path in file_paths):
  566. return True
  567. return False
  568. @property
  569. def known_patterns(self) -> List[Tuple[Pattern[str], str]]:
  570. if self._known_patterns is not None:
  571. return self._known_patterns
  572. self._known_patterns = []
  573. pattern_sections = [STDLIB] + [section for section in self.sections if section != STDLIB]
  574. for placement in reversed(pattern_sections):
  575. known_placement = KNOWN_SECTION_MAPPING.get(placement, placement).lower()
  576. config_key = f"{KNOWN_PREFIX}{known_placement}"
  577. known_modules = getattr(self, config_key, self.known_other.get(known_placement, ()))
  578. extra_modules = getattr(self, f"extra_{known_placement}", ())
  579. all_modules = set(extra_modules).union(known_modules)
  580. known_patterns = [
  581. pattern
  582. for known_pattern in all_modules
  583. for pattern in self._parse_known_pattern(known_pattern)
  584. ]
  585. for known_pattern in known_patterns:
  586. regexp = "^" + known_pattern.replace("*", ".*").replace("?", ".?") + "$"
  587. self._known_patterns.append((re.compile(regexp), placement))
  588. return self._known_patterns
  589. @property
  590. def section_comments(self) -> Tuple[str, ...]:
  591. if self._section_comments is not None:
  592. return self._section_comments
  593. self._section_comments = tuple(f"# {heading}" for heading in self.import_headings.values())
  594. return self._section_comments
  595. @property
  596. def section_comments_end(self) -> Tuple[str, ...]:
  597. if self._section_comments_end is not None:
  598. return self._section_comments_end
  599. self._section_comments_end = tuple(f"# {footer}" for footer in self.import_footers.values())
  600. return self._section_comments_end
  601. @property
  602. def skips(self) -> FrozenSet[str]:
  603. if self._skips is not None:
  604. return self._skips
  605. self._skips = self.skip.union(self.extend_skip)
  606. return self._skips
  607. @property
  608. def skip_globs(self) -> FrozenSet[str]:
  609. if self._skip_globs is not None:
  610. return self._skip_globs
  611. self._skip_globs = self.skip_glob.union(self.extend_skip_glob)
  612. return self._skip_globs
  613. @property
  614. def sorting_function(self) -> Callable[..., List[str]]:
  615. if self._sorting_function is not None:
  616. return self._sorting_function
  617. if self.sort_order == "natural":
  618. self._sorting_function = sorting.naturally
  619. elif self.sort_order == "native":
  620. self._sorting_function = sorted
  621. else:
  622. available_sort_orders = ["natural", "native"]
  623. import pkg_resources
  624. for sort_plugin in pkg_resources.iter_entry_points("isort.sort_function"):
  625. available_sort_orders.append(sort_plugin.name)
  626. if sort_plugin.name == self.sort_order:
  627. self._sorting_function = sort_plugin.load()
  628. break
  629. else:
  630. raise SortingFunctionDoesNotExist(self.sort_order, available_sort_orders)
  631. return self._sorting_function
  632. def _parse_known_pattern(self, pattern: str) -> List[str]:
  633. """Expand pattern if identified as a directory and return found sub packages"""
  634. if pattern.endswith(os.path.sep):
  635. patterns = [
  636. filename
  637. for filename in os.listdir(os.path.join(self.directory, pattern))
  638. if os.path.isdir(os.path.join(self.directory, pattern, filename))
  639. ]
  640. else:
  641. patterns = [pattern]
  642. return patterns
  643. def _get_str_to_type_converter(setting_name: str) -> Union[Callable[[str], Any], Type[Any]]:
  644. type_converter: Union[Callable[[str], Any], Type[Any]] = type(
  645. _DEFAULT_SETTINGS.get(setting_name, "")
  646. )
  647. if type_converter == WrapModes:
  648. type_converter = wrap_mode_from_string
  649. return type_converter
  650. def _as_list(value: str) -> List[str]:
  651. if isinstance(value, list):
  652. return [item.strip() for item in value]
  653. filtered = [item.strip() for item in value.replace("\n", ",").split(",") if item.strip()]
  654. return filtered
  655. def _abspaths(cwd: str, values: Iterable[str]) -> Set[str]:
  656. paths = {
  657. os.path.join(cwd, value)
  658. if not value.startswith(os.path.sep) and value.endswith(os.path.sep)
  659. else value
  660. for value in values
  661. }
  662. return paths
  663. @lru_cache()
  664. def _find_config(path: str) -> Tuple[str, Dict[str, Any]]:
  665. current_directory = path
  666. tries = 0
  667. while current_directory and tries < MAX_CONFIG_SEARCH_DEPTH:
  668. for config_file_name in CONFIG_SOURCES:
  669. potential_config_file = os.path.join(current_directory, config_file_name)
  670. if os.path.isfile(potential_config_file):
  671. config_data: Dict[str, Any]
  672. try:
  673. config_data = _get_config_data(
  674. potential_config_file, CONFIG_SECTIONS[config_file_name]
  675. )
  676. except Exception:
  677. warn(f"Failed to pull configuration information from {potential_config_file}")
  678. config_data = {}
  679. if config_data:
  680. return (current_directory, config_data)
  681. for stop_dir in STOP_CONFIG_SEARCH_ON_DIRS:
  682. if os.path.isdir(os.path.join(current_directory, stop_dir)):
  683. return (current_directory, {})
  684. new_directory = os.path.split(current_directory)[0]
  685. if new_directory == current_directory:
  686. break
  687. current_directory = new_directory
  688. tries += 1
  689. return (path, {})
  690. @lru_cache()
  691. def find_all_configs(path: str) -> Trie:
  692. """
  693. Looks for config files in the path provided and in all of its sub-directories.
  694. Parses and stores any config file encountered in a trie and returns the root of
  695. the trie
  696. """
  697. trie_root = Trie("default", {})
  698. for (dirpath, _, _) in os.walk(path):
  699. for config_file_name in CONFIG_SOURCES:
  700. potential_config_file = os.path.join(dirpath, config_file_name)
  701. if os.path.isfile(potential_config_file):
  702. config_data: Dict[str, Any]
  703. try:
  704. config_data = _get_config_data(
  705. potential_config_file, CONFIG_SECTIONS[config_file_name]
  706. )
  707. except Exception:
  708. warn(f"Failed to pull configuration information from {potential_config_file}")
  709. config_data = {}
  710. if config_data:
  711. trie_root.insert(potential_config_file, config_data)
  712. break
  713. return trie_root
  714. @lru_cache()
  715. def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]:
  716. settings: Dict[str, Any] = {}
  717. if file_path.endswith(".toml"):
  718. with open(file_path, "rb") as bin_config_file:
  719. config = tomli.load(bin_config_file)
  720. for section in sections:
  721. config_section = config
  722. for key in section.split("."):
  723. config_section = config_section.get(key, {})
  724. settings.update(config_section)
  725. else:
  726. with open(file_path, encoding="utf-8") as config_file:
  727. if file_path.endswith(".editorconfig"):
  728. line = "\n"
  729. last_position = config_file.tell()
  730. while line:
  731. line = config_file.readline()
  732. if "[" in line:
  733. config_file.seek(last_position)
  734. break
  735. last_position = config_file.tell()
  736. config = configparser.ConfigParser(strict=False)
  737. config.read_file(config_file)
  738. for section in sections:
  739. if section.startswith("*.{") and section.endswith("}"):
  740. extension = section[len("*.{") : -1]
  741. for config_key in config.keys():
  742. if (
  743. config_key.startswith("*.{")
  744. and config_key.endswith("}")
  745. and extension
  746. in map(
  747. lambda text: text.strip(), config_key[len("*.{") : -1].split(",") # type: ignore # noqa
  748. )
  749. ):
  750. settings.update(config.items(config_key))
  751. elif config.has_section(section):
  752. settings.update(config.items(section))
  753. if settings:
  754. settings["source"] = file_path
  755. if file_path.endswith(".editorconfig"):
  756. indent_style = settings.pop("indent_style", "").strip()
  757. indent_size = settings.pop("indent_size", "").strip()
  758. if indent_size == "tab":
  759. indent_size = settings.pop("tab_width", "").strip()
  760. if indent_style == "space":
  761. settings["indent"] = " " * (indent_size and int(indent_size) or 4)
  762. elif indent_style == "tab":
  763. settings["indent"] = "\t" * (indent_size and int(indent_size) or 1)
  764. max_line_length = settings.pop("max_line_length", "").strip()
  765. if max_line_length and (max_line_length == "off" or max_line_length.isdigit()):
  766. settings["line_length"] = (
  767. float("inf") if max_line_length == "off" else int(max_line_length)
  768. )
  769. settings = {
  770. key: value
  771. for key, value in settings.items()
  772. if key in _DEFAULT_SETTINGS.keys() or key.startswith(KNOWN_PREFIX)
  773. }
  774. for key, value in settings.items():
  775. existing_value_type = _get_str_to_type_converter(key)
  776. if existing_value_type == tuple:
  777. settings[key] = tuple(_as_list(value))
  778. elif existing_value_type == frozenset:
  779. settings[key] = frozenset(_as_list(settings.get(key))) # type: ignore
  780. elif existing_value_type == bool:
  781. # Only some configuration formats support native boolean values.
  782. if not isinstance(value, bool):
  783. value = _as_bool(value)
  784. settings[key] = value
  785. elif key.startswith(KNOWN_PREFIX):
  786. settings[key] = _abspaths(os.path.dirname(file_path), _as_list(value))
  787. elif key == "force_grid_wrap":
  788. try:
  789. result = existing_value_type(value)
  790. except ValueError: # backwards compatibility for true / false force grid wrap
  791. result = 0 if value.lower().strip() == "false" else 2
  792. settings[key] = result
  793. elif key == "comment_prefix":
  794. settings[key] = str(value).strip("'").strip('"')
  795. else:
  796. settings[key] = existing_value_type(value)
  797. return settings
  798. def _as_bool(value: str) -> bool:
  799. """Given a string value that represents True or False, returns the Boolean equivalent.
  800. Heavily inspired from distutils strtobool.
  801. """
  802. try:
  803. return _STR_BOOLEAN_MAPPING[value.lower()]
  804. except KeyError:
  805. raise ValueError(f"invalid truth value {value}")
  806. DEFAULT_CONFIG = Config()