-
Notifications
You must be signed in to change notification settings - Fork 280
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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. Also add some plugin support test cases, an example plugin as a reference, and doc section about how to develop rules' plugins. Signed-off-by: Satoru SATOH <satoru.satoh@gmail.com> Co-authored-by: Adrien Vergé
- Loading branch information
Showing
12 changed files
with
491 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
# -*- 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.common import RuleTestCase | ||
from tests.yamllint_plugin_example import rules as example | ||
|
||
import yamllint.plugins | ||
import yamllint.rules | ||
|
||
|
||
class FakeEntryPoint(object): | ||
"""Fake object to mimic pkg_resources.EntryPoint. | ||
""" | ||
RULES = example.RULES | ||
|
||
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.forbid_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.forbid_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)) | ||
|
||
@unittest.skipIf(not mock, "unittest.mock is not available") | ||
def test_load_plugin_rules_itr(self): | ||
fun = yamllint.plugins.load_plugin_rules_itr | ||
entry_points = 'pkg_resources.iter_entry_points' | ||
|
||
with mock.patch(entry_points) as iter_entry_points: | ||
iter_entry_points.return_value = [] | ||
self.assertEqual(list(fun()), []) | ||
|
||
iter_entry_points.return_value = [FakeEntryPoint(), | ||
FakeEntryPoint()] | ||
self.assertEqual(sorted(fun()), sorted(FakeEntryPoint.RULES)) | ||
|
||
iter_entry_points.return_value = [BrokenEntryPoint()] | ||
with warnings.catch_warnings(record=True) as warn: | ||
warnings.simplefilter("always") | ||
self.assertEqual(list(fun()), []) | ||
|
||
self.assertEqual(len(warn), 1) | ||
self.assertTrue(issubclass(warn[-1].category, RuntimeWarning)) | ||
self.assertTrue("Could not load the plugin:" | ||
in str(warn[-1].message)) | ||
|
||
|
||
@unittest.skipIf(not mock, "unittest.mock is not available") | ||
class RulesTestCase(unittest.TestCase): | ||
def test_get_default_rule(self): | ||
self.assertEqual(yamllint.rules.get(yamllint.rules.braces.ID), | ||
yamllint.rules.braces) | ||
|
||
def test_get_rule_does_not_exist(self): | ||
with self.assertRaises(ValueError): | ||
yamllint.rules.get('DOESNT_EXIST') | ||
|
||
def test_get_default_rule_with_plugins(self): | ||
with mock.patch.dict(yamllint.rules._EXTERNAL_RULES, example.RULES): | ||
self.assertEqual(yamllint.rules.get(yamllint.rules.braces.ID), | ||
yamllint.rules.braces) | ||
|
||
def test_get_plugin_rules(self): | ||
plugin_rule_id = example.forbid_comments.ID | ||
plugin_rule_mod = example.forbid_comments | ||
|
||
with mock.patch.dict(yamllint.rules._EXTERNAL_RULES, example.RULES): | ||
self.assertEqual(yamllint.rules.get(plugin_rule_id), | ||
plugin_rule_mod) | ||
|
||
def test_get_rule_does_not_exist_with_plugins(self): | ||
with mock.patch.dict(yamllint.rules._EXTERNAL_RULES, example.RULES): | ||
with self.assertRaises(ValueError): | ||
yamllint.rules.get('DOESNT_EXIST') | ||
|
||
|
||
@unittest.skipIf(not mock, "unittest.mock is not available") | ||
class PluginTestCase(RuleTestCase): | ||
def check(self, source, conf, **kwargs): | ||
with mock.patch.dict(yamllint.rules._EXTERNAL_RULES, example.RULES): | ||
super(PluginTestCase, self).check(source, conf, **kwargs) | ||
|
||
|
||
@unittest.skipIf(not mock, 'unittest.mock is not available') | ||
class ForbidCommentPluginTestCase(PluginTestCase): | ||
rule_id = 'forbid-comments' | ||
|
||
def test_plugin_disabled(self): | ||
conf = 'forbid-comments: disable\n' | ||
self.check('---\n' | ||
'# comment\n', conf) | ||
|
||
def test_disabled(self): | ||
conf = ('forbid-comments:\n' | ||
' forbid: false\n') | ||
self.check('---\n' | ||
'# comment\n', conf) | ||
|
||
def test_enabled(self): | ||
conf = ('forbid-comments:\n' | ||
' forbid: true\n') | ||
self.check('---\n' | ||
'# comment\n', conf, problem=(2, 1)) | ||
|
||
|
||
@unittest.skipIf(not mock, 'unittest.mock is not available') | ||
class NoFortyTwoPluginTestCase(PluginTestCase): | ||
rule_id = 'no-forty-two' | ||
|
||
def test_disabled(self): | ||
conf = 'no-forty-two: disable' | ||
self.check('---\n' | ||
'a: 42\n', conf) | ||
|
||
def test_enabled(self): | ||
conf = 'no-forty-two: enable' | ||
self.check('---\n' | ||
'a: 42\n', conf, problem=(2, 4)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
yamllint plugin example | ||
======================= | ||
|
||
This is a yamllint plugin example as a reference, contains the following rules. | ||
|
||
- ``forbid-comments`` to forbid comments | ||
- ``random-failure`` to fail randomly | ||
|
||
To enable thes rules in yamllint, you must add them to your `yamllint config | ||
file <https://yamllint.readthedocs.io/en/stable/configuration.html>`_: | ||
|
||
.. code-block:: yaml | ||
extends: default | ||
rules: | ||
forbid-comments: enable | ||
random-failure: enable | ||
How to develop rule plugins | ||
--------------------------- | ||
|
||
yamllint rule plugins must satisfy the followings. | ||
|
||
#. It must be a Python package installable using pip and distributed under | ||
GPLv3+ same as yamllint. | ||
|
||
How to make a Python package is beyond the scope of this README file. Please | ||
refer to the official guide (`Python Packaging User Guide | ||
<https://packaging.python.org/>`_ ) and related documents. | ||
|
||
#. It must contains the entry point configuration in ``setup.cfg`` or something | ||
similar packaging configuration files, to make it installed and working as a | ||
yamllint plugin like below. (``<plugin_name>`` is that plugin name and | ||
``<plugin_src_dir>`` is a dir where the rule modules exist.) | ||
:: | ||
|
||
[options.entry_points] | ||
yamllint.plugins.rules = | ||
<plugin_name> = <plugin_src_dir> | ||
|
||
#. It must contain custom yamllint rule modules: | ||
|
||
- Each rule module must define a couple of global variables, ``ID`` and | ||
``TYPE``. ``ID`` must not conflicts with other rules' IDs. | ||
- Each rule module must define a function named 'check' to test input data | ||
complies with the rule. | ||
- Each rule module may have other global variables. | ||
- ``CONF`` to define its configuration parameters and those types. | ||
- ``DEFAULT`` to provide default values for each configuration parameters. | ||
|
||
#. It must define a global variable ``RULES`` to provide an iterable object, a | ||
tuple or a list for example, of tuples of rule ID and rule modules to | ||
yamllint like this. | ||
:: | ||
|
||
RULES = ( | ||
# (rule module ID, rule module) | ||
(a_custom_rule_module.ID, a_custom_rule_module), | ||
(other_custom_rule_module.ID, other_custom_rule_module), | ||
) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# -*- 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 ( | ||
forbid_comments, no_forty_two, random_failure | ||
) | ||
|
||
|
||
RULES = ( | ||
(forbid_comments.ID, forbid_comments), | ||
(no_forty_two.ID, no_forty_two), | ||
(random_failure.ID, random_failure) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
# | ||
# 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 forbid comments. | ||
.. rubric:: Options | ||
* Use ``forbid`` to control comments. Set to ``true`` to forbid comments | ||
completely. | ||
.. rubric:: Examples | ||
#. With ``forbid-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: | ||
forbid-comments: | ||
forbid: False | ||
""" | ||
from yamllint.linter import LintProblem | ||
|
||
|
||
ID = 'forbid-comments' | ||
TYPE = 'comment' | ||
CONF = {'forbid': bool} | ||
DEFAULT = {'forbid': False} | ||
|
||
|
||
def check(conf, comment): | ||
if conf['forbid']: | ||
yield LintProblem(comment.line_no, comment.column_no, | ||
'forbidden comment') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# | ||
# 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 forbid 42 in any values. | ||
.. rubric:: Examples | ||
#. With ``no-forty-two: {}`` | ||
the following code snippet would **PASS**: | ||
:: | ||
the_answer: 1 | ||
the following code snippet would **FAIL**: | ||
:: | ||
the_answer: 42 | ||
""" | ||
import yaml | ||
|
||
from yamllint.linter import LintProblem | ||
|
||
|
||
ID = 'no-forty-two' | ||
TYPE = 'token' | ||
|
||
|
||
def check(conf, token, prev, next, nextnext, context): | ||
if (isinstance(token, yaml.ScalarToken) and | ||
isinstance(prev, yaml.ValueToken) and | ||
token.value == '42'): | ||
yield LintProblem(token.start_mark.line + 1, | ||
token.start_mark.column + 1, | ||
'42 is forbidden value') |
Oops, something went wrong.