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