From 0b985a063a63edd3255610cf53b25bb32bf2aa36 Mon Sep 17 00:00:00 2001 From: Peter Ericson Date: Thu, 22 Sep 2016 21:49:09 +1000 Subject: [PATCH] Add the ability to run custom rules included in a project --- docs/configuration.rst | 1 + docs/custom_rules.rst | 62 ++++++++++++++++++++++++++++++++ docs/index.rst | 1 + tests/rules/test_custom.py | 73 ++++++++++++++++++++++++++++++++++++++ yamllint/cli.py | 2 ++ yamllint/rules/__init__.py | 13 +++++++ 6 files changed, 152 insertions(+) create mode 100644 docs/custom_rules.rst create mode 100644 tests/rules/test_custom.py diff --git a/docs/configuration.rst b/docs/configuration.rst index 661e506..75feffc 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -15,6 +15,7 @@ If ``-c`` is not provided, yamllint will look for a configuration file in the following locations (by order of preference): - ``.yamllint`` in the current working directory +- ``.yamllint/config`` in the current working directory - ``$XDG_CONFIG_HOME/yamllint/config`` - ``~/.config/yamllint/config`` diff --git a/docs/custom_rules.rst b/docs/custom_rules.rst new file mode 100644 index 0000000..3b60df0 --- /dev/null +++ b/docs/custom_rules.rst @@ -0,0 +1,62 @@ +Custom Rules +============ + +There are times when you might like to add custom rules to your +project. This could be because the rules you'd like to enforce are +not general enough to consider including in upstream yamllint. + +yamllint will look for custom rules in ``.yamllint/rules``. To enable +a custom rule you need to explicitly reference the rule in your +config. + +Example +~~~~~~~ + +In this example there is a custom rule called ``truthy`` that will +complain if ambiguous truthy values are not quoted. + +This is the directory structure: + +.. code:: plain + + . + |-- .yamllint + | |-- config + | `-- rules + | |-- __init__.py + | `-- truthy.py + `-- example.yml + + 2 directories, 4 files + +This is an example yaml file with ambiguous truthy values: + +.. code:: yaml + + --- + a: y + b: yes + c: on + d: True + +This is an example config file: + +.. code:: yaml + + --- + extends: default + + rules: + truthy: enable + +Lint problems from the custom rule are now included in the yamllint +output: + +.. code:: plain + + $ yamllint example.yml + example.yml + 2:3 error ambiguous truthy value is not quoted (truthy) + 3:3 error ambiguous truthy value is not quoted (truthy) + 4:3 error ambiguous truthy value is not quoted (truthy) + 5:3 error ambiguous truthy value is not quoted (truthy) diff --git a/docs/index.rst b/docs/index.rst index 97e1fc7..cb3e5aa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,3 +26,4 @@ Table of contents disable_with_comments development text_editors + custom_rules diff --git a/tests/rules/test_custom.py b/tests/rules/test_custom.py new file mode 100644 index 0000000..e8cc500 --- /dev/null +++ b/tests/rules/test_custom.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 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 . + +import os +import shutil +import tempfile + +from tests.common import RuleTestCase +from yamllint.config import YamlLintConfigError + + +class CustomTestCase(RuleTestCase): + rule_id = 'custom' + + @classmethod + def setUpClass(self): + self.tmpd = tempfile.mkdtemp() + rules = os.path.join(self.tmpd, '.yamllint', 'rules') + os.makedirs(rules) + + with open(os.path.join(rules, '__init__.py'), 'w'): + pass + + with open(os.path.join(rules, 'custom.py'), 'w') as f: + f.write("""ID = 'custom' +TYPE = 'token' + +def check(*args, **kwargs): + if 0: + yield +""") + + self.orig_cwd = os.getcwd() + os.chdir(self.tmpd) + + def test_disabled(self): + conf = 'custom: disable\n' + + self.check('---\n', conf) + + def test_enabled(self): + conf = 'custom: enable\n' + + self.check('---\n', conf) + + def test_config_present(self): + conf = 'custom: enable\n' + + self.check('---\n', conf) + + def test_config_missing(self): + conf = '' + + with self.assertRaises(YamlLintConfigError): + self.check('---\n', conf) + + @classmethod + def tearDownClass(self): + os.chdir(self.orig_cwd) + shutil.rmtree(self.tmpd) diff --git a/yamllint/cli.py b/yamllint/cli.py index 9e3e5f3..bbdaa74 100644 --- a/yamllint/cli.py +++ b/yamllint/cli.py @@ -113,6 +113,8 @@ def run(argv=None): conf = YamlLintConfig(file=args.config_file) elif os.path.isfile('.yamllint'): conf = YamlLintConfig(file='.yamllint') + elif os.path.isfile('.yamllint/config'): + conf = YamlLintConfig(file='.yamllint/config') elif os.path.isfile(user_global_config): conf = YamlLintConfig(file=user_global_config) else: diff --git a/yamllint/rules/__init__.py b/yamllint/rules/__init__.py index 6e82970..e796987 100644 --- a/yamllint/rules/__init__.py +++ b/yamllint/rules/__init__.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import importlib +import os +import sys + from yamllint.rules import ( braces, brackets, @@ -54,6 +58,15 @@ _RULES = { def get(id): + if id not in _RULES: + try: + if os.path.isdir('.yamllint'): + sys.path.append('.yamllint') + module = importlib.import_module('rules.' + id) + _RULES[module.ID] = module + except ImportError as exc: + pass + if id not in _RULES: raise ValueError('no such rule: "%s"' % id)