Compare commits

...

4 Commits

Author SHA1 Message Date
Adrien Vergé d32d1f65ad WIP - test by Adrien 4 years ago
Satoru SATOH 9f9e282da5 enhancement: add a doc section about how to develop rules' plugins
Add a section into the doc about how to develop rules' plugins.
4 years ago
Satoru SATOH 6abce4e9a9 enhancement: enable rules' plugin support
Enable rules' plugin support and add its test cases.
4 years ago
Satoru SATOH 1c15ad1adc 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>
4 years ago

@ -16,3 +16,47 @@ Basic example of running the linter from Python:
.. automodule:: yamllint.linter
:members:
Develop rule plugins
---------------------
yamllint provides a plugin mechanism using setuptools (pkg_resources) to allow
adding custom rules. So, you can extend yamllint and add rules with your own
custom yamllint rule plugins if you developed them.
Yamllint rule plugins must satisfy the followings.
#. It must be a Python package installable using pip and distributed under
GPLv3+ same as yamllint.
#. 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' ID.
- 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_MAP to provide mappings of rule ID
and rule modules to yamllint like this.
::
RULES_MAP = {
# rule ID: rule module
a_custom_rule.ID: a_custom_rule
}
To develop yamllint rules, the default rules themselves in yamllint may become
good references.

@ -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,11 @@
[metadata]
name = yamllint_plugin_example
version = 1.0.0
[options]
packages = find:
install_requires = yamllint
[options.entry_points]
yamllint.plugins.rules =
example = yamllint_plugin_example

@ -0,0 +1,2 @@
import setuptools
setuptools.setup()

@ -0,0 +1,27 @@
# -*- 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, random_failure
RULES_MAP = {
override_comments.ID: override_comments,
random_failure.ID: random_failure,
}

@ -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,31 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2020 Adrien Vergé
#
# 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 random
from yamllint.linter import LintProblem
ID = 'random-failure'
TYPE = 'token'
def check(conf, token, prev, next, nextnext, context):
if random.random() > 0.9:
yield LintProblem(token.start_mark.line + 1,
token.start_mark.column + 1,
'random failure')

@ -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,64 @@
# -*- 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
try:
from unittest import mock
except ImportError: # for python 2.7
mock = False
from tests.plugins import example
import yamllint.rules
RULE_NEVER_EXISTS = "rule_never_exists"
PLUGIN_RULES = example.RULES_MAP
class TestCase(unittest.TestCase):
"""Test cases for yamllint.rules.__init__.*.
"""
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(RULE_NEVER_EXISTS)
@unittest.skipIf(not mock, "unittest.mock is not available")
class TestCaseUsingMock(unittest.TestCase):
"""Test cases for yamllint.rules.__init__.* using mock.
"""
def test_get_default_rule_with_plugins(self):
with mock.patch.dict(yamllint.rules._EXTERNAL_RULES, PLUGIN_RULES):
self.assertEqual(yamllint.rules.get(yamllint.rules.braces.ID),
yamllint.rules.braces)
def test_get_plugin_rules(self):
plugin_rule_id = example.override_comments.ID
plugin_rule_mod = example.override_comments
with mock.patch.dict(yamllint.rules._EXTERNAL_RULES, PLUGIN_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, PLUGIN_RULES):
with self.assertRaises(ValueError):
yamllint.rules.get(RULE_NEVER_EXISTS)

@ -29,5 +29,7 @@ rules:
octal-values: disable
quoted-strings: disable
trailing-spaces: enable
random-failure: enable
override-comments: {forbid: true}
truthy:
level: warning

@ -0,0 +1,62 @@
# -*- 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
print(rule_id, rule_mod)###
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())

@ -14,6 +14,7 @@
# 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 yamllint.plugins
from yamllint.rules import (
braces,
brackets,
@ -62,9 +63,14 @@ _RULES = {
truthy.ID: truthy,
}
_EXTERNAL_RULES = yamllint.plugins.get_plugin_rules_map()
def get(id):
if id not in _RULES:
raise ValueError('no such rule: "%s"' % id)
return _RULES[id]
def get(rule_id):
if rule_id in _RULES:
return _RULES[rule_id]
if rule_id in _EXTERNAL_RULES:
return _EXTERNAL_RULES[rule_id]
raise ValueError('no such rule: "%s"' % rule_id)

Loading…
Cancel
Save