enhancement: add lint rules plugin support
Add plugin support using setuptools (pkg_resources) plugin mechanism to yamllint to allow users to add their own custom lint rule plugins, together with an example plugin implementation and test cases. Signed-off-by: Satoru SATOH <satoru.satoh@gmail.com>feature/plugin-2020-10-02
parent
85c8631183
commit
1c15ad1adc
@ -0,0 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2020 Satoru SATOH
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""yamllint plugin entry point
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from . import override_comments
|
||||||
|
|
||||||
|
|
||||||
|
RULES_MAP = {
|
||||||
|
override_comments.ID: override_comments
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2020 Satoru SATOH
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Use this rule to override some comments' rules.
|
||||||
|
|
||||||
|
.. rubric:: Options
|
||||||
|
|
||||||
|
* Use ``forbid`` to control comments. Set to ``true`` to forbid comments
|
||||||
|
completely.
|
||||||
|
|
||||||
|
.. rubric:: Examples
|
||||||
|
|
||||||
|
#. With ``override-comments: {forbid: true}``
|
||||||
|
|
||||||
|
the following code snippet would **PASS**:
|
||||||
|
::
|
||||||
|
|
||||||
|
foo: 1
|
||||||
|
|
||||||
|
the following code snippet would **FAIL**:
|
||||||
|
::
|
||||||
|
|
||||||
|
# baz
|
||||||
|
foo: 1
|
||||||
|
|
||||||
|
.. rubric:: Default values (when enabled)
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
rules:
|
||||||
|
override-comments:
|
||||||
|
forbid: False
|
||||||
|
|
||||||
|
"""
|
||||||
|
from yamllint.linter import LintProblem
|
||||||
|
|
||||||
|
|
||||||
|
ID = 'override-comments'
|
||||||
|
TYPE = 'comment'
|
||||||
|
CONF = {'forbid': bool}
|
||||||
|
DEFAULT = {'forbid': False}
|
||||||
|
|
||||||
|
|
||||||
|
def check(conf, comment):
|
||||||
|
"""Check if comments are found.
|
||||||
|
"""
|
||||||
|
if conf['forbid']:
|
||||||
|
yield LintProblem(comment.line_no, comment.column_no,
|
||||||
|
'forbidden comment')
|
@ -0,0 +1,80 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2020 Satoru SATOH
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
import unittest
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
try:
|
||||||
|
from unittest import mock
|
||||||
|
except ImportError: # for python 2.7
|
||||||
|
mock = False
|
||||||
|
|
||||||
|
from tests.plugins import example
|
||||||
|
|
||||||
|
import yamllint.plugins
|
||||||
|
|
||||||
|
|
||||||
|
class FakeEntryPoint(object):
|
||||||
|
"""Fake object to mimic pkg_resources.EntryPoint.
|
||||||
|
"""
|
||||||
|
RULES_MAP = example.RULES_MAP
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
"""Fake method to return self.
|
||||||
|
"""
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class BrokenEntryPoint(FakeEntryPoint):
|
||||||
|
"""Fake object to mimic load failure of pkg_resources.EntryPoint.
|
||||||
|
"""
|
||||||
|
def load(self):
|
||||||
|
raise ImportError("This entry point should fail always!")
|
||||||
|
|
||||||
|
|
||||||
|
class PluginFunctionsTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_validate_rule_module(self):
|
||||||
|
fun = yamllint.plugins.validate_rule_module
|
||||||
|
rule_mod = example.override_comments
|
||||||
|
|
||||||
|
self.assertFalse(fun(object()))
|
||||||
|
self.assertTrue(fun(rule_mod))
|
||||||
|
|
||||||
|
@unittest.skipIf(not mock, "unittest.mock is not available")
|
||||||
|
def test_validate_rule_module_using_mock(self):
|
||||||
|
fun = yamllint.plugins.validate_rule_module
|
||||||
|
rule_mod = example.override_comments
|
||||||
|
|
||||||
|
with mock.patch.object(rule_mod, "ID", False):
|
||||||
|
self.assertFalse(fun(rule_mod))
|
||||||
|
|
||||||
|
with mock.patch.object(rule_mod, "TYPE", False):
|
||||||
|
self.assertFalse(fun(rule_mod))
|
||||||
|
|
||||||
|
with mock.patch.object(rule_mod, "check", True):
|
||||||
|
self.assertFalse(fun(rule_mod))
|
||||||
|
|
||||||
|
def test_load_plugin_rules_itr(self):
|
||||||
|
fun = yamllint.plugins.load_plugin_rules_itr
|
||||||
|
|
||||||
|
self.assertEqual(list(fun([])), [])
|
||||||
|
self.assertEqual(sorted(fun([FakeEntryPoint(),
|
||||||
|
FakeEntryPoint()])),
|
||||||
|
sorted(FakeEntryPoint.RULES_MAP.items()))
|
||||||
|
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
|
self.assertEqual(list(fun([BrokenEntryPoint()])), [])
|
@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2020 Satoru SATOH
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
Plugin module utilizing setuptools (pkg_resources) to allow users to add their
|
||||||
|
own custom lint rules.
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
|
|
||||||
|
PACKAGE_GROUP = "yamllint.plugins.rules"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_rule_module(rule_mod):
|
||||||
|
"""Test if given rule module is valid.
|
||||||
|
"""
|
||||||
|
return (getattr(rule_mod, "ID", False) and
|
||||||
|
getattr(rule_mod, "TYPE", False)
|
||||||
|
) and callable(getattr(rule_mod, "check", False))
|
||||||
|
|
||||||
|
|
||||||
|
def load_plugin_rules_itr(entry_points=None, group=PACKAGE_GROUP):
|
||||||
|
"""Load custom lint rule plugins."""
|
||||||
|
if not entry_points:
|
||||||
|
entry_points = pkg_resources.iter_entry_points(group)
|
||||||
|
|
||||||
|
rule_ids = set()
|
||||||
|
for entry in entry_points:
|
||||||
|
try:
|
||||||
|
rules = entry.load()
|
||||||
|
for rule_id, rule_mod in rules.RULES_MAP.items():
|
||||||
|
if rule_id in rule_ids or not validate_rule_module(rule_mod):
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield (rule_id, rule_mod)
|
||||||
|
rule_ids.add(rule_id)
|
||||||
|
|
||||||
|
# pkg_resources.EntryPoint.resolve may throw ImportError.
|
||||||
|
except (AttributeError, ImportError):
|
||||||
|
warnings.warn("Could not load the plugin: {}".format(entry),
|
||||||
|
RuntimeWarning)
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_rules_map():
|
||||||
|
"""Get a mappings of plugin rule's IDs and rules."""
|
||||||
|
return dict((rule_id, rule_mod)
|
||||||
|
for rule_id, rule_mod in load_plugin_rules_itr())
|
Loading…
Reference in New Issue