okay fine

This commit is contained in:
pacnpal
2024-11-03 17:47:26 +00:00
parent 01c6004a79
commit 27eb239e97
10020 changed files with 1935769 additions and 2364 deletions

View File

@@ -0,0 +1,2 @@
"""Submodule of built-in plugins and plugin managers."""
from __future__ import annotations

View File

@@ -0,0 +1,365 @@
"""Functions related to finding and loading plugins."""
from __future__ import annotations
import configparser
import importlib.metadata
import inspect
import itertools
import logging
import sys
from typing import Any
from typing import Generator
from typing import Iterable
from typing import NamedTuple
from flake8 import utils
from flake8.defaults import VALID_CODE_PREFIX
from flake8.exceptions import ExecutionError
from flake8.exceptions import FailedToLoadPlugin
LOG = logging.getLogger(__name__)
FLAKE8_GROUPS = frozenset(("flake8.extension", "flake8.report"))
BANNED_PLUGINS = {
"flake8-colors": "5.0",
"flake8-per-file-ignores": "3.7",
}
class Plugin(NamedTuple):
"""A plugin before loading."""
package: str
version: str
entry_point: importlib.metadata.EntryPoint
class LoadedPlugin(NamedTuple):
"""Represents a plugin after being imported."""
plugin: Plugin
obj: Any
parameters: dict[str, bool]
@property
def entry_name(self) -> str:
"""Return the name given in the packaging metadata."""
return self.plugin.entry_point.name
@property
def display_name(self) -> str:
"""Return the name for use in user-facing / error messages."""
return f"{self.plugin.package}[{self.entry_name}]"
class Checkers(NamedTuple):
"""Classified plugins needed for checking."""
tree: list[LoadedPlugin]
logical_line: list[LoadedPlugin]
physical_line: list[LoadedPlugin]
class Plugins(NamedTuple):
"""Classified plugins."""
checkers: Checkers
reporters: dict[str, LoadedPlugin]
disabled: list[LoadedPlugin]
def all_plugins(self) -> Generator[LoadedPlugin, None, None]:
"""Return an iterator over all :class:`LoadedPlugin`s."""
yield from self.checkers.tree
yield from self.checkers.logical_line
yield from self.checkers.physical_line
yield from self.reporters.values()
def versions_str(self) -> str:
"""Return a user-displayed list of plugin versions."""
return ", ".join(
sorted(
{
f"{loaded.plugin.package}: {loaded.plugin.version}"
for loaded in self.all_plugins()
if loaded.plugin.package not in {"flake8", "local"}
}
)
)
class PluginOptions(NamedTuple):
"""Options related to plugin loading."""
local_plugin_paths: tuple[str, ...]
enable_extensions: frozenset[str]
require_plugins: frozenset[str]
@classmethod
def blank(cls) -> PluginOptions:
"""Make a blank PluginOptions, mostly used for tests."""
return cls(
local_plugin_paths=(),
enable_extensions=frozenset(),
require_plugins=frozenset(),
)
def _parse_option(
cfg: configparser.RawConfigParser,
cfg_opt_name: str,
opt: str | None,
) -> list[str]:
# specified on commandline: use that
if opt is not None:
return utils.parse_comma_separated_list(opt)
else:
# ideally this would reuse our config parsing framework but we need to
# parse this from preliminary options before plugins are enabled
for opt_name in (cfg_opt_name, cfg_opt_name.replace("_", "-")):
val = cfg.get("flake8", opt_name, fallback=None)
if val is not None:
return utils.parse_comma_separated_list(val)
else:
return []
def parse_plugin_options(
cfg: configparser.RawConfigParser,
cfg_dir: str,
*,
enable_extensions: str | None,
require_plugins: str | None,
) -> PluginOptions:
"""Parse plugin loading related options."""
paths_s = cfg.get("flake8:local-plugins", "paths", fallback="").strip()
paths = utils.parse_comma_separated_list(paths_s)
paths = utils.normalize_paths(paths, cfg_dir)
return PluginOptions(
local_plugin_paths=tuple(paths),
enable_extensions=frozenset(
_parse_option(cfg, "enable_extensions", enable_extensions),
),
require_plugins=frozenset(
_parse_option(cfg, "require_plugins", require_plugins),
),
)
def _flake8_plugins(
eps: Iterable[importlib.metadata.EntryPoint],
name: str,
version: str,
) -> Generator[Plugin, None, None]:
pyflakes_meta = importlib.metadata.distribution("pyflakes").metadata
pycodestyle_meta = importlib.metadata.distribution("pycodestyle").metadata
for ep in eps:
if ep.group not in FLAKE8_GROUPS:
continue
if ep.name == "F":
yield Plugin(pyflakes_meta["name"], pyflakes_meta["version"], ep)
elif ep.name in "EW":
# pycodestyle provides both `E` and `W` -- but our default select
# handles those
# ideally pycodestyle's plugin entrypoints would exactly represent
# the codes they produce...
yield Plugin(
pycodestyle_meta["name"], pycodestyle_meta["version"], ep
)
else:
yield Plugin(name, version, ep)
def _find_importlib_plugins() -> Generator[Plugin, None, None]:
# some misconfigured pythons (RHEL) have things on `sys.path` twice
seen = set()
for dist in importlib.metadata.distributions():
# assigned to prevent continual reparsing
eps = dist.entry_points
# perf: skip parsing `.metadata` (slow) if no entry points match
if not any(ep.group in FLAKE8_GROUPS for ep in eps):
continue
# assigned to prevent continual reparsing
meta = dist.metadata
if meta["name"] in seen:
continue
else:
seen.add(meta["name"])
if meta["name"] in BANNED_PLUGINS:
LOG.warning(
"%s plugin is obsolete in flake8>=%s",
meta["name"],
BANNED_PLUGINS[meta["name"]],
)
continue
elif meta["name"] == "flake8":
# special case flake8 which provides plugins for pyflakes /
# pycodestyle
yield from _flake8_plugins(eps, meta["name"], meta["version"])
continue
for ep in eps:
if ep.group in FLAKE8_GROUPS:
yield Plugin(meta["name"], meta["version"], ep)
def _find_local_plugins(
cfg: configparser.RawConfigParser,
) -> Generator[Plugin, None, None]:
for plugin_type in ("extension", "report"):
group = f"flake8.{plugin_type}"
for plugin_s in utils.parse_comma_separated_list(
cfg.get("flake8:local-plugins", plugin_type, fallback="").strip(),
regexp=utils.LOCAL_PLUGIN_LIST_RE,
):
name, _, entry_str = plugin_s.partition("=")
name, entry_str = name.strip(), entry_str.strip()
ep = importlib.metadata.EntryPoint(name, entry_str, group)
yield Plugin("local", "local", ep)
def _check_required_plugins(
plugins: list[Plugin],
expected: frozenset[str],
) -> None:
plugin_names = {
utils.normalize_pypi_name(plugin.package) for plugin in plugins
}
expected_names = {utils.normalize_pypi_name(name) for name in expected}
missing_plugins = expected_names - plugin_names
if missing_plugins:
raise ExecutionError(
f"required plugins were not installed!\n"
f"- installed: {', '.join(sorted(plugin_names))}\n"
f"- expected: {', '.join(sorted(expected_names))}\n"
f"- missing: {', '.join(sorted(missing_plugins))}"
)
def find_plugins(
cfg: configparser.RawConfigParser,
opts: PluginOptions,
) -> list[Plugin]:
"""Discovers all plugins (but does not load them)."""
ret = [*_find_importlib_plugins(), *_find_local_plugins(cfg)]
# for determinism, sort the list
ret.sort()
_check_required_plugins(ret, opts.require_plugins)
return ret
def _parameters_for(func: Any) -> dict[str, bool]:
"""Return the parameters for the plugin.
This will inspect the plugin and return either the function parameters
if the plugin is a function or the parameters for ``__init__`` after
``self`` if the plugin is a class.
:returns:
A dictionary mapping the parameter name to whether or not it is
required (a.k.a., is positional only/does not have a default).
"""
is_class = not inspect.isfunction(func)
if is_class:
func = func.__init__
parameters = {
parameter.name: parameter.default is inspect.Parameter.empty
for parameter in inspect.signature(func).parameters.values()
if parameter.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
}
if is_class:
parameters.pop("self", None)
return parameters
def _load_plugin(plugin: Plugin) -> LoadedPlugin:
try:
obj = plugin.entry_point.load()
except Exception as e:
raise FailedToLoadPlugin(plugin.package, e)
if not callable(obj):
err = TypeError("expected loaded plugin to be callable")
raise FailedToLoadPlugin(plugin.package, err)
return LoadedPlugin(plugin, obj, _parameters_for(obj))
def _import_plugins(
plugins: list[Plugin],
opts: PluginOptions,
) -> list[LoadedPlugin]:
sys.path.extend(opts.local_plugin_paths)
return [_load_plugin(p) for p in plugins]
def _classify_plugins(
plugins: list[LoadedPlugin],
opts: PluginOptions,
) -> Plugins:
tree = []
logical_line = []
physical_line = []
reporters = {}
disabled = []
for loaded in plugins:
if (
getattr(loaded.obj, "off_by_default", False)
and loaded.plugin.entry_point.name not in opts.enable_extensions
):
disabled.append(loaded)
elif loaded.plugin.entry_point.group == "flake8.report":
reporters[loaded.entry_name] = loaded
elif "tree" in loaded.parameters:
tree.append(loaded)
elif "logical_line" in loaded.parameters:
logical_line.append(loaded)
elif "physical_line" in loaded.parameters:
physical_line.append(loaded)
else:
raise NotImplementedError(f"what plugin type? {loaded}")
for loaded in itertools.chain(tree, logical_line, physical_line):
if not VALID_CODE_PREFIX.match(loaded.entry_name):
raise ExecutionError(
f"plugin code for `{loaded.display_name}` does not match "
f"{VALID_CODE_PREFIX.pattern}"
)
return Plugins(
checkers=Checkers(
tree=tree,
logical_line=logical_line,
physical_line=physical_line,
),
reporters=reporters,
disabled=disabled,
)
def load_plugins(
plugins: list[Plugin],
opts: PluginOptions,
) -> Plugins:
"""Load and classify all flake8 plugins.
- first: extends ``sys.path`` with ``paths`` (to import local plugins)
- next: converts the ``Plugin``s to ``LoadedPlugins``
- finally: classifies plugins into their specific types
"""
return _classify_plugins(_import_plugins(plugins, opts), opts)

View File

@@ -0,0 +1,112 @@
"""Generated using ./bin/gen-pycodestyle-plugin."""
# fmt: off
from __future__ import annotations
from typing import Any
from typing import Generator
from pycodestyle import ambiguous_identifier as _ambiguous_identifier
from pycodestyle import bare_except as _bare_except
from pycodestyle import blank_lines as _blank_lines
from pycodestyle import break_after_binary_operator as _break_after_binary_operator # noqa: E501
from pycodestyle import break_before_binary_operator as _break_before_binary_operator # noqa: E501
from pycodestyle import comparison_negative as _comparison_negative
from pycodestyle import comparison_to_singleton as _comparison_to_singleton
from pycodestyle import comparison_type as _comparison_type
from pycodestyle import compound_statements as _compound_statements
from pycodestyle import continued_indentation as _continued_indentation
from pycodestyle import explicit_line_join as _explicit_line_join
from pycodestyle import extraneous_whitespace as _extraneous_whitespace
from pycodestyle import imports_on_separate_lines as _imports_on_separate_lines
from pycodestyle import indentation as _indentation
from pycodestyle import maximum_doc_length as _maximum_doc_length
from pycodestyle import maximum_line_length as _maximum_line_length
from pycodestyle import missing_whitespace as _missing_whitespace
from pycodestyle import missing_whitespace_after_keyword as _missing_whitespace_after_keyword # noqa: E501
from pycodestyle import module_imports_on_top_of_file as _module_imports_on_top_of_file # noqa: E501
from pycodestyle import python_3000_invalid_escape_sequence as _python_3000_invalid_escape_sequence # noqa: E501
from pycodestyle import tabs_obsolete as _tabs_obsolete
from pycodestyle import tabs_or_spaces as _tabs_or_spaces
from pycodestyle import trailing_blank_lines as _trailing_blank_lines
from pycodestyle import trailing_whitespace as _trailing_whitespace
from pycodestyle import whitespace_around_comma as _whitespace_around_comma
from pycodestyle import whitespace_around_keywords as _whitespace_around_keywords # noqa: E501
from pycodestyle import whitespace_around_named_parameter_equals as _whitespace_around_named_parameter_equals # noqa: E501
from pycodestyle import whitespace_around_operator as _whitespace_around_operator # noqa: E501
from pycodestyle import whitespace_before_comment as _whitespace_before_comment
from pycodestyle import whitespace_before_parameters as _whitespace_before_parameters # noqa: E501
def pycodestyle_logical(
blank_before: Any,
blank_lines: Any,
checker_state: Any,
hang_closing: Any,
indent_char: Any,
indent_level: Any,
indent_size: Any,
line_number: Any,
lines: Any,
logical_line: Any,
max_doc_length: Any,
noqa: Any,
previous_indent_level: Any,
previous_logical: Any,
previous_unindented_logical_line: Any,
tokens: Any,
verbose: Any,
) -> Generator[tuple[int, str], None, None]:
"""Run pycodestyle logical checks."""
yield from _ambiguous_identifier(logical_line, tokens)
yield from _bare_except(logical_line, noqa)
yield from _blank_lines(logical_line, blank_lines, indent_level, line_number, blank_before, previous_logical, previous_unindented_logical_line, previous_indent_level, lines) # noqa: E501
yield from _break_after_binary_operator(logical_line, tokens)
yield from _break_before_binary_operator(logical_line, tokens)
yield from _comparison_negative(logical_line)
yield from _comparison_to_singleton(logical_line, noqa)
yield from _comparison_type(logical_line, noqa)
yield from _compound_statements(logical_line)
yield from _continued_indentation(logical_line, tokens, indent_level, hang_closing, indent_char, indent_size, noqa, verbose) # noqa: E501
yield from _explicit_line_join(logical_line, tokens)
yield from _extraneous_whitespace(logical_line)
yield from _imports_on_separate_lines(logical_line)
yield from _indentation(logical_line, previous_logical, indent_char, indent_level, previous_indent_level, indent_size) # noqa: E501
yield from _maximum_doc_length(logical_line, max_doc_length, noqa, tokens)
yield from _missing_whitespace(logical_line, tokens)
yield from _missing_whitespace_after_keyword(logical_line, tokens)
yield from _module_imports_on_top_of_file(logical_line, indent_level, checker_state, noqa) # noqa: E501
yield from _python_3000_invalid_escape_sequence(logical_line, tokens, noqa)
yield from _whitespace_around_comma(logical_line)
yield from _whitespace_around_keywords(logical_line)
yield from _whitespace_around_named_parameter_equals(logical_line, tokens)
yield from _whitespace_around_operator(logical_line)
yield from _whitespace_before_comment(logical_line, tokens)
yield from _whitespace_before_parameters(logical_line, tokens)
def pycodestyle_physical(
indent_char: Any,
line_number: Any,
lines: Any,
max_line_length: Any,
multiline: Any,
noqa: Any,
physical_line: Any,
total_lines: Any,
) -> Generator[tuple[int, str], None, None]:
"""Run pycodestyle physical checks."""
ret = _maximum_line_length(physical_line, max_line_length, multiline, line_number, noqa) # noqa: E501
if ret is not None:
yield ret
ret = _tabs_obsolete(physical_line)
if ret is not None:
yield ret
ret = _tabs_or_spaces(physical_line, indent_char)
if ret is not None:
yield ret
ret = _trailing_blank_lines(physical_line, lines, line_number, total_lines)
if ret is not None:
yield ret
ret = _trailing_whitespace(physical_line)
if ret is not None:
yield ret

View File

@@ -0,0 +1,112 @@
"""Plugin built-in to Flake8 to treat pyflakes as a plugin."""
from __future__ import annotations
import argparse
import ast
import logging
from typing import Any
from typing import Generator
import pyflakes.checker
from flake8.options.manager import OptionManager
LOG = logging.getLogger(__name__)
FLAKE8_PYFLAKES_CODES = {
"UnusedImport": "F401",
"ImportShadowedByLoopVar": "F402",
"ImportStarUsed": "F403",
"LateFutureImport": "F404",
"ImportStarUsage": "F405",
"ImportStarNotPermitted": "F406",
"FutureFeatureNotDefined": "F407",
"PercentFormatInvalidFormat": "F501",
"PercentFormatExpectedMapping": "F502",
"PercentFormatExpectedSequence": "F503",
"PercentFormatExtraNamedArguments": "F504",
"PercentFormatMissingArgument": "F505",
"PercentFormatMixedPositionalAndNamed": "F506",
"PercentFormatPositionalCountMismatch": "F507",
"PercentFormatStarRequiresSequence": "F508",
"PercentFormatUnsupportedFormatCharacter": "F509",
"StringDotFormatInvalidFormat": "F521",
"StringDotFormatExtraNamedArguments": "F522",
"StringDotFormatExtraPositionalArguments": "F523",
"StringDotFormatMissingArgument": "F524",
"StringDotFormatMixingAutomatic": "F525",
"FStringMissingPlaceholders": "F541",
"MultiValueRepeatedKeyLiteral": "F601",
"MultiValueRepeatedKeyVariable": "F602",
"TooManyExpressionsInStarredAssignment": "F621",
"TwoStarredExpressions": "F622",
"AssertTuple": "F631",
"IsLiteral": "F632",
"InvalidPrintSyntax": "F633",
"IfTuple": "F634",
"BreakOutsideLoop": "F701",
"ContinueOutsideLoop": "F702",
"YieldOutsideFunction": "F704",
"ReturnOutsideFunction": "F706",
"DefaultExceptNotLast": "F707",
"DoctestSyntaxError": "F721",
"ForwardAnnotationSyntaxError": "F722",
"RedefinedWhileUnused": "F811",
"UndefinedName": "F821",
"UndefinedExport": "F822",
"UndefinedLocal": "F823",
"DuplicateArgument": "F831",
"UnusedVariable": "F841",
"UnusedAnnotation": "F842",
"RaiseNotImplemented": "F901",
}
class FlakesChecker(pyflakes.checker.Checker):
"""Subclass the Pyflakes checker to conform with the flake8 API."""
with_doctest = False
def __init__(self, tree: ast.AST, filename: str) -> None:
"""Initialize the PyFlakes plugin with an AST tree and filename."""
super().__init__(
tree, filename=filename, withDoctest=self.with_doctest
)
@classmethod
def add_options(cls, parser: OptionManager) -> None:
"""Register options for PyFlakes on the Flake8 OptionManager."""
parser.add_option(
"--builtins",
parse_from_config=True,
comma_separated_list=True,
help="define more built-ins, comma separated",
)
parser.add_option(
"--doctests",
default=False,
action="store_true",
parse_from_config=True,
help="also check syntax of the doctests",
)
@classmethod
def parse_options(cls, options: argparse.Namespace) -> None:
"""Parse option values from Flake8's OptionManager."""
if options.builtins:
cls.builtIns = cls.builtIns.union(options.builtins)
cls.with_doctest = options.doctests
def run(self) -> Generator[tuple[int, int, str, type[Any]], None, None]:
"""Run the plugin."""
for message in self.messages:
col = getattr(message, "col", 0)
yield (
message.lineno,
col,
"{} {}".format(
FLAKE8_PYFLAKES_CODES.get(type(message).__name__, "F999"),
message.message % message.message_args,
),
message.__class__,
)

View File

@@ -0,0 +1,42 @@
"""Functions for constructing the requested report plugin."""
from __future__ import annotations
import argparse
import logging
from flake8.formatting.base import BaseFormatter
from flake8.plugins.finder import LoadedPlugin
LOG = logging.getLogger(__name__)
def make(
reporters: dict[str, LoadedPlugin],
options: argparse.Namespace,
) -> BaseFormatter:
"""Make the formatter from the requested user options.
- if :option:`flake8 --quiet` is specified, return the ``quiet-filename``
formatter.
- if :option:`flake8 --quiet` is specified at least twice, return the
``quiet-nothing`` formatter.
- otherwise attempt to return the formatter by name.
- failing that, assume it is a format string and return the ``default``
formatter.
"""
format_name = options.format
if options.quiet == 1:
format_name = "quiet-filename"
elif options.quiet >= 2:
format_name = "quiet-nothing"
try:
format_plugin = reporters[format_name]
except KeyError:
LOG.warning(
"%r is an unknown formatter. Falling back to default.",
format_name,
)
format_plugin = reporters["default"]
return format_plugin.obj(options)