flake8/tests/unit/plugins/finder_test.py
2021-12-31 15:09:54 -08:00

610 lines
16 KiB
Python

import configparser
import sys
from unittest import mock
import pytest
from flake8._compat import importlib_metadata
from flake8.exceptions import FailedToLoadPlugin
from flake8.plugins import finder
from flake8.plugins.pyflakes import FlakesChecker
def _ep(name="X", value="dne:dne", group="flake8.extension"):
return importlib_metadata.EntryPoint(name, value, group)
def _plugin(package="local", version="local", ep=None):
if ep is None:
ep = _ep()
return finder.Plugin(package, version, ep)
def _loaded(plugin=None, obj=None, parameters=None):
if plugin is None:
plugin = _plugin()
if parameters is None:
parameters = {"tree": True}
return finder.LoadedPlugin(plugin, obj, parameters)
def test_loaded_plugin_entry_name_vs_display_name():
loaded = _loaded(_plugin(package="package-name", ep=_ep(name="Q")))
assert loaded.entry_name == "Q"
assert loaded.display_name == "package-name[Q]"
def test_plugins_all_plugins():
tree_plugin = _loaded(parameters={"tree": True})
logical_line_plugin = _loaded(parameters={"logical_line": True})
physical_line_plugin = _loaded(parameters={"physical_line": True})
report_plugin = _loaded(
plugin=_plugin(ep=_ep(name="R", group="flake8.report"))
)
plugins = finder.Plugins(
checkers=finder.Checkers(
tree=[tree_plugin],
logical_line=[logical_line_plugin],
physical_line=[physical_line_plugin],
),
reporters={"R": report_plugin},
)
assert tuple(plugins.all_plugins()) == (
tree_plugin,
logical_line_plugin,
physical_line_plugin,
report_plugin,
)
def test_plugins_versions_str():
plugins = finder.Plugins(
checkers=finder.Checkers(
tree=[_loaded(_plugin(package="pkg1", version="1"))],
logical_line=[_loaded(_plugin(package="pkg2", version="2"))],
physical_line=[_loaded(_plugin(package="pkg1", version="1"))],
),
reporters={
# ignore flake8 builtin plugins
"default": _loaded(_plugin(package="flake8")),
# ignore local plugins
"custom": _loaded(_plugin(package="local")),
},
)
assert plugins.versions_str() == "pkg1: 1, pkg2: 2"
@pytest.fixture
def pyflakes_dist(tmp_path):
metadata = """\
Metadata-Version: 2.1
Name: pyflakes
Version: 9000.1.0
"""
d = tmp_path.joinpath("pyflakes.dist-info")
d.mkdir()
d.joinpath("METADATA").write_text(metadata)
return importlib_metadata.PathDistribution(d)
@pytest.fixture
def pycodestyle_dist(tmp_path):
metadata = """\
Metadata-Version: 2.1
Name: pycodestyle
Version: 9000.2.0
"""
d = tmp_path.joinpath("pycodestyle.dist-info")
d.mkdir()
d.joinpath("METADATA").write_text(metadata)
return importlib_metadata.PathDistribution(d)
@pytest.fixture
def flake8_dist(tmp_path):
metadata = """\
Metadata-Version: 2.1
Name: flake8
Version: 9001
"""
entry_points = """\
[console_scripts]
flake8 = flake8.main.cli:main
[flake8.extension]
F = flake8.plugins.pyflakes:FlakesChecker
pycodestyle.bare_except = pycodestyle:bare_except
pycodestyle.blank_lines = pycodestyle:blank_lines
[flake8.report]
default = flake8.formatting.default:Default
pylint = flake8.formatting.default:Pylint
"""
d = tmp_path.joinpath("flake8.dist-info")
d.mkdir()
d.joinpath("METADATA").write_text(metadata)
d.joinpath("entry_points.txt").write_text(entry_points)
return importlib_metadata.PathDistribution(d)
@pytest.fixture
def flake8_foo_dist(tmp_path):
metadata = """\
Metadata-Version: 2.1
Name: flake8-foo
Version: 1.2.3
"""
eps = """\
[console_scripts]
foo = flake8_foo:main
[flake8.extension]
Q = flake8_foo:Plugin
[flake8.report]
foo = flake8_foo:Formatter
"""
d = tmp_path.joinpath("flake8_foo.dist-info")
d.mkdir()
d.joinpath("METADATA").write_text(metadata)
d.joinpath("entry_points.txt").write_text(eps)
return importlib_metadata.PathDistribution(d)
@pytest.fixture
def mock_distribution(pyflakes_dist, pycodestyle_dist):
dists = {"pyflakes": pyflakes_dist, "pycodestyle": pycodestyle_dist}
with mock.patch.object(importlib_metadata, "distribution", dists.get):
yield
def test_flake8_plugins(flake8_dist, mock_distribution):
"""Ensure entrypoints for flake8 are parsed specially."""
eps = flake8_dist.entry_points
ret = set(finder._flake8_plugins(eps, "flake8", "9001"))
assert ret == {
finder.Plugin(
"pyflakes",
"9000.1.0",
importlib_metadata.EntryPoint(
"F",
"flake8.plugins.pyflakes:FlakesChecker",
"flake8.extension",
),
),
finder.Plugin(
"pycodestyle",
"9000.2.0",
importlib_metadata.EntryPoint(
"pycodestyle.bare_except",
"pycodestyle:bare_except",
"flake8.extension",
),
),
finder.Plugin(
"pycodestyle",
"9000.2.0",
importlib_metadata.EntryPoint(
"pycodestyle.blank_lines",
"pycodestyle:blank_lines",
"flake8.extension",
),
),
finder.Plugin(
"flake8",
"9001",
importlib_metadata.EntryPoint(
"default", "flake8.formatting.default:Default", "flake8.report"
),
),
finder.Plugin(
"flake8",
"9001",
importlib_metadata.EntryPoint(
"pylint", "flake8.formatting.default:Pylint", "flake8.report"
),
),
}
def test_importlib_plugins(
tmp_path,
flake8_dist,
flake8_foo_dist,
mock_distribution,
caplog,
):
"""Ensure we can load plugins from importlib_metadata."""
# make sure flake8-colors is skipped
flake8_colors_metadata = """\
Metadata-Version: 2.1
Name: flake8-colors
Version: 1.2.3
"""
flake8_colors_eps = """\
[flake8.extension]
flake8-colors = flake8_colors:ColorFormatter
"""
flake8_colors_d = tmp_path.joinpath("flake8_colors.dist-info")
flake8_colors_d.mkdir()
flake8_colors_d.joinpath("METADATA").write_text(flake8_colors_metadata)
flake8_colors_d.joinpath("entry_points.txt").write_text(flake8_colors_eps)
flake8_colors_dist = importlib_metadata.PathDistribution(flake8_colors_d)
unrelated_metadata = """\
Metadata-Version: 2.1
Name: unrelated
Version: 4.5.6
"""
unrelated_eps = """\
[console_scripts]
unrelated = unrelated:main
"""
unrelated_d = tmp_path.joinpath("unrelated.dist-info")
unrelated_d.mkdir()
unrelated_d.joinpath("METADATA").write_text(unrelated_metadata)
unrelated_d.joinpath("entry_points.txt").write_text(unrelated_eps)
unrelated_dist = importlib_metadata.PathDistribution(unrelated_d)
with mock.patch.object(
importlib_metadata,
"distributions",
return_value=[
flake8_dist,
flake8_colors_dist,
flake8_foo_dist,
unrelated_dist,
],
):
ret = set(finder._find_importlib_plugins())
assert ret == {
finder.Plugin(
"flake8-foo",
"1.2.3",
importlib_metadata.EntryPoint(
"Q", "flake8_foo:Plugin", "flake8.extension"
),
),
finder.Plugin(
"pycodestyle",
"9000.2.0",
importlib_metadata.EntryPoint(
"pycodestyle.bare_except",
"pycodestyle:bare_except",
"flake8.extension",
),
),
finder.Plugin(
"pycodestyle",
"9000.2.0",
importlib_metadata.EntryPoint(
"pycodestyle.blank_lines",
"pycodestyle:blank_lines",
"flake8.extension",
),
),
finder.Plugin(
"pyflakes",
"9000.1.0",
importlib_metadata.EntryPoint(
"F",
"flake8.plugins.pyflakes:FlakesChecker",
"flake8.extension",
),
),
finder.Plugin(
"flake8",
"9001",
importlib_metadata.EntryPoint(
"default", "flake8.formatting.default:Default", "flake8.report"
),
),
finder.Plugin(
"flake8",
"9001",
importlib_metadata.EntryPoint(
"pylint", "flake8.formatting.default:Pylint", "flake8.report"
),
),
finder.Plugin(
"flake8-foo",
"1.2.3",
importlib_metadata.EntryPoint(
"foo", "flake8_foo:Formatter", "flake8.report"
),
),
}
assert caplog.record_tuples == [
(
"flake8.plugins.finder",
30,
"flake8-colors plugin is obsolete in flake8>=4.1",
),
]
def test_find_local_plugins_nothing():
cfg = configparser.RawConfigParser()
assert set(finder._find_local_plugins(cfg)) == set()
@pytest.fixture
def local_plugin_cfg():
cfg = configparser.RawConfigParser()
cfg.add_section("flake8:local-plugins")
cfg.set("flake8:local-plugins", "extension", "Y=mod2:attr, X = mod:attr")
cfg.set("flake8:local-plugins", "report", "Z=mod3:attr")
return cfg
def test_find_local_plugins(local_plugin_cfg):
ret = set(finder._find_local_plugins(local_plugin_cfg))
assert ret == {
finder.Plugin(
"local",
"local",
importlib_metadata.EntryPoint(
"X",
"mod:attr",
"flake8.extension",
),
),
finder.Plugin(
"local",
"local",
importlib_metadata.EntryPoint(
"Y",
"mod2:attr",
"flake8.extension",
),
),
finder.Plugin(
"local",
"local",
importlib_metadata.EntryPoint(
"Z",
"mod3:attr",
"flake8.report",
),
),
}
def test_find_plugins(
tmp_path,
flake8_dist,
flake8_foo_dist,
mock_distribution,
local_plugin_cfg,
):
with mock.patch.object(
importlib_metadata,
"distributions",
return_value=[flake8_dist, flake8_foo_dist],
):
ret = finder.find_plugins(local_plugin_cfg)
assert ret == [
finder.Plugin(
"flake8",
"9001",
importlib_metadata.EntryPoint(
"default", "flake8.formatting.default:Default", "flake8.report"
),
),
finder.Plugin(
"flake8",
"9001",
importlib_metadata.EntryPoint(
"pylint", "flake8.formatting.default:Pylint", "flake8.report"
),
),
finder.Plugin(
"flake8-foo",
"1.2.3",
importlib_metadata.EntryPoint(
"Q", "flake8_foo:Plugin", "flake8.extension"
),
),
finder.Plugin(
"flake8-foo",
"1.2.3",
importlib_metadata.EntryPoint(
"foo", "flake8_foo:Formatter", "flake8.report"
),
),
finder.Plugin(
"local",
"local",
importlib_metadata.EntryPoint("X", "mod:attr", "flake8.extension"),
),
finder.Plugin(
"local",
"local",
importlib_metadata.EntryPoint(
"Y", "mod2:attr", "flake8.extension"
),
),
finder.Plugin(
"local",
"local",
importlib_metadata.EntryPoint("Z", "mod3:attr", "flake8.report"),
),
finder.Plugin(
"pycodestyle",
"9000.2.0",
importlib_metadata.EntryPoint(
"pycodestyle.bare_except",
"pycodestyle:bare_except",
"flake8.extension",
),
),
finder.Plugin(
"pycodestyle",
"9000.2.0",
importlib_metadata.EntryPoint(
"pycodestyle.blank_lines",
"pycodestyle:blank_lines",
"flake8.extension",
),
),
finder.Plugin(
"pyflakes",
"9000.1.0",
importlib_metadata.EntryPoint(
"F",
"flake8.plugins.pyflakes:FlakesChecker",
"flake8.extension",
),
),
]
def test_find_local_plugin_paths_missing(tmp_path):
cfg = configparser.RawConfigParser()
assert finder.find_local_plugin_paths(cfg, str(tmp_path)) == []
def test_find_local_plugin_paths(tmp_path):
cfg = configparser.RawConfigParser()
cfg.add_section("flake8:local-plugins")
cfg.set("flake8:local-plugins", "paths", "./a, ./b")
ret = finder.find_local_plugin_paths(cfg, str(tmp_path))
assert ret == [str(tmp_path.joinpath("a")), str(tmp_path.joinpath("b"))]
def test_parameters_for_class_plugin():
"""Verify that we can retrieve the parameters for a class plugin."""
class FakeCheck:
def __init__(self, tree):
raise NotImplementedError
assert finder._parameters_for(FakeCheck) == {"tree": True}
def test_parameters_for_function_plugin():
"""Verify that we retrieve the parameters for a function plugin."""
def fake_plugin(physical_line, self, tree, optional=None):
raise NotImplementedError
assert finder._parameters_for(fake_plugin) == {
"physical_line": True,
"self": True,
"tree": True,
"optional": False,
}
def test_load_plugin_import_error():
plugin = _plugin(ep=_ep(value="dne:dne"))
with pytest.raises(FailedToLoadPlugin) as excinfo:
finder._load_plugin(plugin)
pkg, e = excinfo.value.args
assert pkg == "local"
assert isinstance(e, ModuleNotFoundError)
def test_load_plugin_not_callable():
plugin = _plugin(ep=_ep(value="os:curdir"))
with pytest.raises(FailedToLoadPlugin) as excinfo:
finder._load_plugin(plugin)
pkg, e = excinfo.value.args
assert pkg == "local"
assert isinstance(e, TypeError)
assert e.args == ("expected loaded plugin to be callable",)
def test_load_plugin_ok():
plugin = _plugin(ep=_ep(value="flake8.plugins.pyflakes:FlakesChecker"))
loaded = finder._load_plugin(plugin)
assert loaded == finder.LoadedPlugin(
plugin,
FlakesChecker,
{"tree": True, "file_tokens": True, "filename": True},
)
@pytest.fixture
def reset_sys():
orig_path = sys.path[:]
orig_modules = sys.modules.copy()
yield
sys.path[:] = orig_path
sys.modules.clear()
sys.modules.update(orig_modules)
@pytest.mark.usefixtures("reset_sys")
def test_import_plugins_extends_sys_path():
plugin = _plugin(ep=_ep(value="aplugin:ExtensionTestPlugin2"))
ret = finder._import_plugins([plugin], ["tests/integration/subdir"])
import aplugin
assert ret == [
finder.LoadedPlugin(
plugin,
aplugin.ExtensionTestPlugin2,
{"tree": True},
),
]
def test_classify_plugins():
report_plugin = _loaded(
plugin=_plugin(ep=_ep(name="R", group="flake8.report"))
)
tree_plugin = _loaded(parameters={"tree": True})
logical_line_plugin = _loaded(parameters={"logical_line": True})
physical_line_plugin = _loaded(parameters={"physical_line": True})
classified = finder._classify_plugins(
[report_plugin, tree_plugin, logical_line_plugin, physical_line_plugin]
)
assert classified == finder.Plugins(
checkers=finder.Checkers(
tree=[tree_plugin],
logical_line=[logical_line_plugin],
physical_line=[physical_line_plugin],
),
reporters={"R": report_plugin},
)
@pytest.mark.usefixtures("reset_sys")
def test_load_plugins():
plugin = _plugin(ep=_ep(value="aplugin:ExtensionTestPlugin2"))
ret = finder.load_plugins([plugin], ["tests/integration/subdir"])
import aplugin
assert ret == finder.Plugins(
checkers=finder.Checkers(
tree=[
finder.LoadedPlugin(
plugin,
aplugin.ExtensionTestPlugin2,
{"tree": True},
),
],
logical_line=[],
physical_line=[],
),
reporters={},
)