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)