Skip to content

Commit 50d6915

Browse files
committed
rework plugin loading
1 parent 38c5ece commit 50d6915

36 files changed

+1278
-1506
lines changed

docs/source/internal/plugin_handling.rst

Lines changed: 11 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -11,85 +11,33 @@ new checks. It now supports:
1111

1212
- alternative report formatters
1313

14-
To facilitate this, |Flake8| needed a more mature way of managing plugins.
15-
Thus, we developed the |PluginManager| which accepts a namespace and will load
16-
the plugins for that namespace. A |PluginManager| creates and manages many
17-
|Plugin| instances.
18-
19-
A |Plugin| lazily loads the underlying entry-point provided by setuptools.
20-
The entry-point will be loaded either by calling
21-
:meth:`~flake8.plugins.manager.Plugin.load_plugin` or accessing the ``plugin``
22-
attribute. We also use this abstraction to retrieve options that the plugin
23-
wishes to register and parse.
24-
25-
The only public method the |PluginManager| provides is
26-
:meth:`~flake8.plugins.manager.PluginManager.map`. This will accept a function
27-
(or other callable) and call it with each plugin as the first parameter.
28-
29-
We build atop the |PluginManager| with the |PTM|. It is expected that users of
30-
the |PTM| will subclass it and specify the ``namespace``, e.g.,
31-
32-
.. code-block:: python
33-
34-
class ExamplePluginType(flake8.plugin.manager.PluginTypeManager):
35-
namespace = 'example-plugins'
36-
37-
This provides a few extra methods via the |PluginManager|'s ``map`` method.
38-
39-
Finally, we create two classes of plugins:
40-
41-
- :class:`~flake8.plugins.manager.Checkers`
42-
43-
- :class:`~flake8.plugins.manager.ReportFormatters`
44-
45-
These are used to interact with each of the types of plugins individually.
46-
47-
.. note::
48-
49-
Our inspiration for our plugin handling comes from the author's extensive
50-
experience with ``stevedore``.
51-
5214
Default Plugins
5315
---------------
5416

5517
Finally, |Flake8| has always provided its own plugin shim for Pyflakes. As
5618
part of that we carry our own shim in-tree and now store that in
5719
:mod:`flake8.plugins.pyflakes`.
5820

59-
|Flake8| also registers plugins for pep8. Each check in pep8 requires
60-
different parameters and it cannot easily be shimmed together like Pyflakes
61-
was. As such, plugins have a concept of a "group". If you look at our
62-
:file:`setup.py` you will see that we register pep8 checks roughly like so:
21+
|Flake8| also registers plugins for pycodestyle. Each check in pycodestyle
22+
requires different parameters and it cannot easily be shimmed together like
23+
Pyflakes was. As such, plugins have a concept of a "group". If you look at our
24+
:file:`setup.py` you will see that we register pycodestyle checks roughly like
25+
so:
6326

6427
.. code::
6528
66-
pep8.<check-name> = pep8:<check-name>
29+
pycodestyle.<check-name> = pycodestyle:<check-name>
6730
6831
We do this to identify that ``<check-name>>`` is part of a group. This also
6932
enables us to special-case how we handle reporting those checks. Instead of
70-
reporting each check in the ``--version`` output, we report ``pep8`` and check
71-
``pep8`` the module for a ``__version__`` attribute. We only report it once
72-
to avoid confusing users.
33+
reporting each check in the ``--version`` output, we only report
34+
``pycodestyle`` once.
7335

7436
API Documentation
7537
-----------------
7638

77-
.. autoclass:: flake8.plugins.manager.PluginManager
78-
:members:
79-
:special-members: __init__
80-
81-
.. autoclass:: flake8.plugins.manager.Plugin
82-
:members:
83-
:special-members: __init__
84-
85-
.. autoclass:: flake8.plugins.manager.PluginTypeManager
86-
:members:
87-
88-
.. autoclass:: flake8.plugins.manager.Checkers
89-
:members:
39+
.. autofunction:: flake8.plugins.finder.find_plugins
9040

91-
.. autoclass:: flake8.plugins.manager.ReportFormatters
41+
.. autofunction:: flake8.plugins.finder.find_local_plugin_paths
9242

93-
.. |PluginManager| replace:: :class:`~flake8.plugins.manager.PluginManager`
94-
.. |Plugin| replace:: :class:`~flake8.plugins.manager.Plugin`
95-
.. |PTM| replace:: :class:`~flake8.plugins.manager.PluginTypeManager`
43+
.. autofunction:: flake8.plugins.finder.load_plugins

docs/source/internal/utils.rst

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -67,23 +67,6 @@ filename matches a single pattern. In our use case, however, we typically have
6767
a list of patterns and want to know if the filename matches any of them. This
6868
function abstracts that logic away with a little extra logic.
6969

70-
.. autofunction:: flake8.utils.parameters_for
71-
72-
|Flake8| analyzes the parameters to plugins to determine what input they are
73-
expecting. Plugins may expect one of the following:
74-
75-
- ``physical_line`` to receive the line as it appears in the file
76-
77-
- ``logical_line`` to receive the logical line (not as it appears in the file)
78-
79-
- ``tree`` to receive the abstract syntax tree (AST) for the file
80-
81-
We also analyze the rest of the parameters to provide more detail to the
82-
plugin. This function will return the parameters in a consistent way across
83-
versions of Python and will handle both classes and functions that are used as
84-
plugins. Further, if the plugin is a class, it will strip the ``self``
85-
argument so we can check the parameters of the plugin consistently.
86-
8770
.. autofunction:: flake8.utils.parse_unified_diff
8871

8972
To handle usage of :option:`flake8 --diff`, |Flake8| needs to be able

docs/source/plugin-development/plugin-parameters.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ Indicating Desired Data
2222
=======================
2323

2424
|Flake8| inspects the plugin's signature to determine what parameters it
25-
expects using :func:`flake8.utils.parameters_for`.
26-
:attr:`flake8.plugins.manager.Plugin.parameters` caches the values so that
25+
expects using :func:`flake8.plugins.finder._parameters_for`.
26+
:attr:`flake8.plugins.finder.LoadedPlugin.parameters` caches the values so that
2727
each plugin makes that fairly expensive call once per plugin. When processing
2828
a file, a plugin can ask for any of the following:
2929

example-plugin/src/flake8_example_plugin/off_by_default.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
class ExampleTwo:
55
"""Second Example Plugin."""
66

7-
name = "off-by-default-example-plugin"
8-
version = "1.0.0"
9-
107
off_by_default = True
118

129
def __init__(self, tree):

example-plugin/src/flake8_example_plugin/on_by_default.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
class ExampleOne:
55
"""First Example Plugin."""
66

7-
name = "on-by-default-example-plugin"
8-
version = "1.0.0"
9-
107
def __init__(self, tree):
118
self.tree = tree
129

pytest.ini

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
[pytest]
2-
norecursedirs = .git .* *.egg* old docs dist build
2+
norecursedirs = .git .* *.egg* docs dist build
33
addopts = -rw
4-
filterwarnings =
5-
error
6-
ignore:SelectableGroups:DeprecationWarning
4+
filterwarnings = error

setup.cfg

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ install_requires =
4343
pycodestyle>=2.8.0,<2.9.0
4444
pyflakes>=2.4.0,<2.5.0
4545
importlib-metadata<4.3;python_version<"3.8"
46-
python_requires = >=3.6
46+
python_requires = >=3.6.1
4747

4848
[options.packages.find]
4949
where = src
@@ -109,12 +109,6 @@ warn_unused_ignores = true
109109
# TODO: fix these
110110
[mypy-flake8.api.legacy]
111111
disallow_untyped_defs = false
112-
[mypy-flake8.checker]
113-
disallow_untyped_defs = false
114-
[mypy-flake8.main.application]
115-
disallow_untyped_defs = false
116-
[mypy-flake8.plugins.manager]
117-
disallow_untyped_defs = false
118112

119113
[mypy-tests.*]
120114
disallow_untyped_defs = false

src/flake8/checker.py

Lines changed: 35 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Checker Manager and Checker classes."""
2+
import argparse
23
import collections
34
import errno
45
import itertools
56
import logging
67
import multiprocessing.pool
78
import signal
89
import tokenize
10+
from typing import Any
911
from typing import Dict
1012
from typing import List
1113
from typing import Optional
@@ -16,6 +18,9 @@
1618
from flake8 import processor
1719
from flake8 import utils
1820
from flake8.discover_files import expand_paths
21+
from flake8.plugins.finder import Checkers
22+
from flake8.plugins.finder import LoadedPlugin
23+
from flake8.style_guide import StyleGuideManager
1924

2025
Results = List[Tuple[str, int, int, str, Optional[str]]]
2126

@@ -56,21 +61,15 @@ class Manager:
5661
together and make our output deterministic.
5762
"""
5863

59-
def __init__(self, style_guide, checker_plugins):
60-
"""Initialize our Manager instance.
61-
62-
:param style_guide:
63-
The instantiated style guide for this instance of Flake8.
64-
:type style_guide:
65-
flake8.style_guide.StyleGuide
66-
:param checker_plugins:
67-
The plugins representing checks parsed from entry-points.
68-
:type checker_plugins:
69-
flake8.plugins.manager.Checkers
70-
"""
64+
def __init__(
65+
self,
66+
style_guide: StyleGuideManager,
67+
plugins: Checkers,
68+
) -> None:
69+
"""Initialize our Manager instance."""
7170
self.style_guide = style_guide
7271
self.options = style_guide.options
73-
self.checks = checker_plugins
72+
self.plugins = plugins
7473
self.jobs = self._job_count()
7574
self._all_checkers: List[FileChecker] = []
7675
self.checkers: List[FileChecker] = []
@@ -158,9 +157,12 @@ def make_checkers(self, paths: Optional[List[str]] = None) -> None:
158157
if paths is None:
159158
paths = self.options.filenames
160159

161-
checks = self.checks.to_dictionary()
162160
self._all_checkers = [
163-
FileChecker(filename, checks, self.options)
161+
FileChecker(
162+
filename=filename,
163+
plugins=self.plugins,
164+
options=self.options,
165+
)
164166
for filename in expand_paths(
165167
paths=paths,
166168
stdin_display_name=self.options.stdin_display_name,
@@ -273,23 +275,17 @@ def stop(self) -> None:
273275
class FileChecker:
274276
"""Manage running checks for a file and aggregate the results."""
275277

276-
def __init__(self, filename, checks, options):
277-
"""Initialize our file checker.
278-
279-
:param str filename:
280-
Name of the file to check.
281-
:param checks:
282-
The plugins registered to check the file.
283-
:type checks:
284-
dict
285-
:param options:
286-
Parsed option values from config and command-line.
287-
:type options:
288-
argparse.Namespace
289-
"""
278+
def __init__(
279+
self,
280+
*,
281+
filename: str,
282+
plugins: Checkers,
283+
options: argparse.Namespace,
284+
) -> None:
285+
"""Initialize our file checker."""
290286
self.options = options
291287
self.filename = filename
292-
self.checks = checks
288+
self.plugins = plugins
293289
self.results: Results = []
294290
self.statistics = {
295291
"tokens": 0,
@@ -342,29 +338,27 @@ def report(
342338
self.results.append((error_code, line_number, column, text, line))
343339
return error_code
344340

345-
def run_check(self, plugin, **arguments):
341+
def run_check(self, plugin: LoadedPlugin, **arguments: Any) -> Any:
346342
"""Run the check in a single plugin."""
347343
LOG.debug("Running %r with %r", plugin, arguments)
348344
assert self.processor is not None
349345
try:
350-
self.processor.keyword_arguments_for(
351-
plugin["parameters"], arguments
352-
)
346+
self.processor.keyword_arguments_for(plugin.parameters, arguments)
353347
except AttributeError as ae:
354348
LOG.error("Plugin requested unknown parameters.")
355349
raise exceptions.PluginRequestedUnknownParameters(
356-
plugin_name=plugin["plugin_name"], exception=ae
350+
plugin_name=plugin.plugin.package, exception=ae
357351
)
358352
try:
359-
return plugin["plugin"](**arguments)
353+
return plugin.obj(**arguments)
360354
except Exception as all_exc:
361355
LOG.critical(
362356
"Plugin %s raised an unexpected exception",
363-
plugin["name"],
357+
plugin.display_name,
364358
exc_info=True,
365359
)
366360
raise exceptions.PluginExecutionFailed(
367-
plugin_name=plugin["plugin_name"], exception=all_exc
361+
plugin_name=plugin.display_name, exception=all_exc
368362
)
369363

370364
@staticmethod
@@ -431,7 +425,7 @@ def run_ast_checks(self) -> None:
431425
assert self.processor is not None
432426
ast = self.processor.build_ast()
433427

434-
for plugin in self.checks["ast_plugins"]:
428+
for plugin in self.plugins.tree:
435429
checker = self.run_check(plugin, tree=ast)
436430
# If the plugin uses a class, call the run method of it, otherwise
437431
# the call should return something iterable itself
@@ -457,7 +451,7 @@ def run_logical_checks(self) -> None:
457451

458452
LOG.debug('Logical line: "%s"', logical_line.rstrip())
459453

460-
for plugin in self.checks["logical_line_plugins"]:
454+
for plugin in self.plugins.logical_line:
461455
self.processor.update_checker_state_for(plugin)
462456
results = self.run_check(plugin, logical_line=logical_line) or ()
463457
for offset, text in results:
@@ -479,7 +473,7 @@ def run_physical_checks(self, physical_line: str) -> None:
479473
A single physical check may return multiple errors.
480474
"""
481475
assert self.processor is not None
482-
for plugin in self.checks["physical_line_plugins"]:
476+
for plugin in self.plugins.physical_line:
483477
self.processor.update_checker_state_for(plugin)
484478
result = self.run_check(plugin, physical_line=physical_line)
485479

0 commit comments

Comments
 (0)