config.py 4.0 KB

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