# built-in
import re
from pathlib import Path
from typing import Any, Dict, List, Tuple, Union
# external
from flake8.utils import fnmatch
REX_NAME = re.compile(r'[-_.]+')
ALIASES = {
'aaa': 'flake8-aaa',
'flake-mutable': 'flake8-mutable',
'flake8-pandas-vet': 'pandas-vet',
'import-order': 'flake8-import-order',
'logging-format': 'flake8-logging-format',
'naming': 'pep8-naming',
'pyflakes': 'pyflakes',
'sql': 'flake8-sql',
'use-fstring-format': 'flake8-use-fstring',
'use-fstring-percent': 'flake8-use-fstring',
'use-fstring-prefix': 'flake8-use-fstring',
}
PluginsType = Dict[str, List[str]]
[docs]def get_plugin_name(plugin: Dict[str, Any]) -> str:
"""Get plugin name from plugin info
Users expect the same plugin name as the name of the package that provides plugin.
However, that's not true for some plugins.
Also, some plugins has different module name, that doesn't match to package.
Lookup order:
1. Ad-hoc aliases when nothing match
2. Normalized name that starts with `flake8`
3. Normalized name that starts with `pep`
4. `plugin_name`
"""
if not plugin:
return 'UNKNOWN'
if plugin['plugin_name'] in ALIASES:
return ALIASES[plugin['plugin_name']]
names = (plugin['plugin_name'], plugin['plugin'].__module__)
names = [REX_NAME.sub('-', name).lower() for name in names]
for name in names:
if name.startswith('flake8'):
return name
for name in names:
if name.startswith('pep8'):
return name
return names[0]
[docs]def get_plugin_rules(plugin_name: str, plugins: PluginsType) -> List[str]:
"""Get rules for plugin from `plugins` in the config
Plugin name can be specified as a glob expression.
So, it's not trivial to match the right one
1. Try to find exact match (normalizing ass all packages names normalized)
2. Try to find globs that match and select the longest one (nginx-style)
3. Return empty list if nothing is found.
"""
if not plugins:
return []
plugin_name = REX_NAME.sub('-', plugin_name).lower()
# try to find exact match
for pattern, rules in plugins.items():
if '*' not in pattern and REX_NAME.sub('-', pattern).lower() == plugin_name:
return rules
# try to find match by pattern and select the longest
best_match = (0, []) # type: Tuple[int, List[str]]
for pattern, rules in plugins.items():
if not fnmatch(filename=plugin_name, patterns=[pattern]):
continue
match = len(pattern)
if match > best_match[0]:
best_match = match, rules
if best_match[0]:
return best_match[1]
return []
[docs]def check_include(code: str, rules: List[str]) -> bool:
"""
0. Validate rules
1. Return True if rule explicitly included
2. Return False if rule explicitly excluded
3. Return True if the latest glob-matching rule is include
4. Return False if the latest glob-matching rule is exclude
"""
# always report exceptions in file processing
if code in ('E902', 'E999'):
return True
for rule in rules:
if len(rule) < 2 or rule[0] not in {'-', '+'}:
raise ValueError('invalid rule: `{}`'.format(rule))
for rule in reversed(rules):
if code.lower() == rule[1:].lower():
return rule[0] == '+'
include = False
for rule in rules:
if fnmatch(code, patterns=[rule[1:]]):
include = rule[0] == '+'
return include
[docs]def get_exceptions(
path: Union[str, Path], exceptions: Dict[str, PluginsType], root: Path = None,
) -> PluginsType:
"""
Just like in get_plugin_rules. The algorithm:
1. Try to find exact match (normalizing as all packages names normalized)
2. Try to find globs that match and select the longest one (nginx-style)
"""
if not exceptions:
return dict()
if isinstance(path, str):
path = Path(path)
if root is None:
root = Path().resolve()
try:
path = path.resolve().relative_to(root).as_posix()
except ValueError:
# path is not in the project root
return dict()
exceptions = sorted(
exceptions.items(),
key=lambda item: len(item[0]),
)
aggregated_rules = dict()
# glob
for path_rule, rules in exceptions:
if '*' not in path_rule:
continue
if fnmatch(filename=path, patterns=[path_rule]):
aggregated_rules.update(rules)
# prefix
for path_rule, rules in exceptions:
if '*' in path_rule:
continue
if path.startswith(path_rule):
aggregated_rules.update(rules)
return aggregated_rules