config.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. """Config handling logic for Flake8."""
  2. from __future__ import annotations
  3. import configparser
  4. import logging
  5. import os.path
  6. from typing import Any
  7. from flake8 import exceptions
  8. from flake8.defaults import VALID_CODE_PREFIX
  9. from flake8.options.manager import OptionManager
  10. LOG = logging.getLogger(__name__)
  11. def _stat_key(s: str) -> tuple[int, int]:
  12. # same as what's used by samefile / samestat
  13. st = os.stat(s)
  14. return st.st_ino, st.st_dev
  15. def _find_config_file(path: str) -> str | None:
  16. # on windows if the homedir isn't detected this returns back `~`
  17. home = os.path.expanduser("~")
  18. try:
  19. home_stat = _stat_key(home) if home != "~" else None
  20. except OSError: # FileNotFoundError / PermissionError / etc.
  21. home_stat = None
  22. dir_stat = _stat_key(path)
  23. while True:
  24. for candidate in ("setup.cfg", "tox.ini", ".flake8"):
  25. cfg = configparser.RawConfigParser()
  26. cfg_path = os.path.join(path, candidate)
  27. try:
  28. cfg.read(cfg_path, encoding="UTF-8")
  29. except (UnicodeDecodeError, configparser.ParsingError) as e:
  30. LOG.warning("ignoring unparseable config %s: %s", cfg_path, e)
  31. else:
  32. # only consider it a config if it contains flake8 sections
  33. if "flake8" in cfg or "flake8:local-plugins" in cfg:
  34. return cfg_path
  35. new_path = os.path.dirname(path)
  36. new_dir_stat = _stat_key(new_path)
  37. if new_dir_stat == dir_stat or new_dir_stat == home_stat:
  38. break
  39. else:
  40. path = new_path
  41. dir_stat = new_dir_stat
  42. # did not find any configuration file
  43. return None
  44. def load_config(
  45. config: str | None,
  46. extra: list[str],
  47. *,
  48. isolated: bool = False,
  49. ) -> tuple[configparser.RawConfigParser, str]:
  50. """Load the configuration given the user options.
  51. - in ``isolated`` mode, return an empty configuration
  52. - if a config file is given in ``config`` use that, otherwise attempt to
  53. discover a configuration using ``tox.ini`` / ``setup.cfg`` / ``.flake8``
  54. - finally, load any ``extra`` configuration files
  55. """
  56. pwd = os.path.abspath(".")
  57. if isolated:
  58. return configparser.RawConfigParser(), pwd
  59. if config is None:
  60. config = _find_config_file(pwd)
  61. cfg = configparser.RawConfigParser()
  62. if config is not None:
  63. if not cfg.read(config, encoding="UTF-8"):
  64. raise exceptions.ExecutionError(
  65. f"The specified config file does not exist: {config}"
  66. )
  67. cfg_dir = os.path.dirname(config)
  68. else:
  69. cfg_dir = pwd
  70. # TODO: remove this and replace it with configuration modifying plugins
  71. # read the additional configs afterwards
  72. for filename in extra:
  73. if not cfg.read(filename, encoding="UTF-8"):
  74. raise exceptions.ExecutionError(
  75. f"The specified config file does not exist: {filename}"
  76. )
  77. return cfg, cfg_dir
  78. def parse_config(
  79. option_manager: OptionManager,
  80. cfg: configparser.RawConfigParser,
  81. cfg_dir: str,
  82. ) -> dict[str, Any]:
  83. """Parse and normalize the typed configuration options."""
  84. if "flake8" not in cfg:
  85. return {}
  86. config_dict = {}
  87. for option_name in cfg["flake8"]:
  88. option = option_manager.config_options_dict.get(option_name)
  89. if option is None:
  90. LOG.debug('Option "%s" is not registered. Ignoring.', option_name)
  91. continue
  92. # Use the appropriate method to parse the config value
  93. value: Any
  94. if option.type is int or option.action == "count":
  95. value = cfg.getint("flake8", option_name)
  96. elif option.action in {"store_true", "store_false"}:
  97. value = cfg.getboolean("flake8", option_name)
  98. else:
  99. value = cfg.get("flake8", option_name)
  100. LOG.debug('Option "%s" returned value: %r', option_name, value)
  101. final_value = option.normalize(value, cfg_dir)
  102. if option_name in {"ignore", "extend-ignore"}:
  103. for error_code in final_value:
  104. if not VALID_CODE_PREFIX.match(error_code):
  105. raise ValueError(
  106. f"Error code {error_code!r} "
  107. f"supplied to {option_name!r} option "
  108. f"does not match {VALID_CODE_PREFIX.pattern!r}"
  109. )
  110. assert option.config_name is not None
  111. config_dict[option.config_name] = final_value
  112. return config_dict