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