diff --git a/tests/plugins/__init__.py b/tests/plugins/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/plugins/example/__init__.py b/tests/plugins/example/__init__.py
new file mode 100644
index 0000000..1ac13cc
--- /dev/null
+++ b/tests/plugins/example/__init__.py
@@ -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 .
+
+"""yamllint plugin entry point
+"""
+from __future__ import absolute_import
+
+from . import override_comments
+
+
+RULES_MAP = {
+ override_comments.ID: override_comments
+}
diff --git a/tests/plugins/example/override_comments.py b/tests/plugins/example/override_comments.py
new file mode 100644
index 0000000..64350eb
--- /dev/null
+++ b/tests/plugins/example/override_comments.py
@@ -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 .
+#
+"""
+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')
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
new file mode 100644
index 0000000..fe59aed
--- /dev/null
+++ b/tests/test_plugins.py
@@ -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 .
+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()])), [])
diff --git a/yamllint/plugins.py b/yamllint/plugins.py
new file mode 100644
index 0000000..fa24218
--- /dev/null
+++ b/yamllint/plugins.py
@@ -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 .
+"""
+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())