diff --git a/tests/rules/common.py b/tests/rules/common.py index cf5ff07..ade842e 100644 --- a/tests/rules/common.py +++ b/tests/rules/common.py @@ -18,7 +18,7 @@ import unittest import yaml -from yamllint.config import parse_config +from yamllint.config import YamlLintConfig from yamllint.errors import LintProblem from yamllint import lint @@ -31,7 +31,7 @@ class RuleTestCase(unittest.TestCase): conf = yaml.safe_load(conf) conf = {'extends': 'default', 'rules': conf} - return parse_config(yaml.safe_dump(conf)) + return YamlLintConfig(yaml.safe_dump(conf)) def check(self, source, conf, **kwargs): expected_problems = [] diff --git a/tests/test_config.py b/tests/test_config.py index 050530f..21d533d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -14,56 +14,164 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os import unittest from yamllint import config -class ConfigTestCase(unittest.TestCase): - def setUp(self): - self.base = config.parse_config_from_file(os.path.join( - os.path.dirname(os.path.dirname(os.path.realpath(__file__))), - 'yamllint', 'conf', 'default.yml')) +class SimpleConfigTestCase(unittest.TestCase): + def test_parse_config(self): + new = config.YamlLintConfig('rules:\n' + ' colons:\n' + ' max-spaces-before: 0\n' + ' max-spaces-after: 1\n') + self.assertEqual(list(new.rules.keys()), ['colons']) + self.assertEqual(new.rules['colons']['max-spaces-before'], 0) + self.assertEqual(new.rules['colons']['max-spaces-after'], 1) + + self.assertEqual(len(new.enabled_rules()), 1) + + def test_unknown_rule(self): + with self.assertRaisesRegexp( + config.YamlLintConfigError, + 'invalid config: no such rule: "this-one-does-not-exist"'): + config.YamlLintConfig('rules:\n' + ' this-one-does-not-exist: {}\n') + + def test_missing_option(self): + with self.assertRaisesRegexp( + config.YamlLintConfigError, + 'invalid config: missing option "max-spaces-before" ' + 'for rule "colons"'): + config.YamlLintConfig('rules:\n' + ' colons:\n' + ' max-spaces-after: 1\n') + + def test_unknown_option(self): + with self.assertRaisesRegexp( + config.YamlLintConfigError, + 'invalid config: unknown option "abcdef" for rule "colons"'): + config.YamlLintConfig('rules:\n' + ' colons:\n' + ' max-spaces-before: 0\n' + ' max-spaces-after: 1\n' + ' abcdef: yes\n') + + +class ExtendedConfigTestCase(unittest.TestCase): + def test_extend_add_rule(self): + old = config.YamlLintConfig('rules:\n' + ' colons:\n' + ' max-spaces-before: 0\n' + ' max-spaces-after: 1\n') + new = config.YamlLintConfig('rules:\n' + ' hyphens:\n' + ' max-spaces-after: 2\n') + new.extend(old) + + self.assertEqual(sorted(new.rules.keys()), ['colons', 'hyphens']) + self.assertEqual(new.rules['colons']['max-spaces-before'], 0) + self.assertEqual(new.rules['colons']['max-spaces-after'], 1) + self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2) + + self.assertEqual(len(new.enabled_rules()), 2) + + def test_extend_remove_rule(self): + old = config.YamlLintConfig('rules:\n' + ' colons:\n' + ' max-spaces-before: 0\n' + ' max-spaces-after: 1\n' + ' hyphens:\n' + ' max-spaces-after: 2\n') + new = config.YamlLintConfig('rules:\n' + ' colons: disable\n') + new.extend(old) + + self.assertEqual(sorted(new.rules.keys()), ['colons', 'hyphens']) + self.assertEqual(new.rules['colons'], False) + self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2) + + self.assertEqual(len(new.enabled_rules()), 1) + + def test_extend_edit_rule(self): + old = config.YamlLintConfig('rules:\n' + ' colons:\n' + ' max-spaces-before: 0\n' + ' max-spaces-after: 1\n' + ' hyphens:\n' + ' max-spaces-after: 2\n') + new = config.YamlLintConfig('rules:\n' + ' colons:\n' + ' max-spaces-before: 3\n' + ' max-spaces-after: 4\n') + new.extend(old) + + self.assertEqual(sorted(new.rules.keys()), ['colons', 'hyphens']) + self.assertEqual(new.rules['colons']['max-spaces-before'], 3) + self.assertEqual(new.rules['colons']['max-spaces-after'], 4) + self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2) + + self.assertEqual(len(new.enabled_rules()), 2) + + def test_extend_reenable_rule(self): + old = config.YamlLintConfig('rules:\n' + ' colons:\n' + ' max-spaces-before: 0\n' + ' max-spaces-after: 1\n' + ' hyphens: disable\n') + new = config.YamlLintConfig('rules:\n' + ' hyphens:\n' + ' max-spaces-after: 2\n') + new.extend(old) + + self.assertEqual(sorted(new.rules.keys()), ['colons', 'hyphens']) + self.assertEqual(new.rules['colons']['max-spaces-before'], 0) + self.assertEqual(new.rules['colons']['max-spaces-after'], 1) + self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2) + + self.assertEqual(len(new.enabled_rules()), 2) + + +class ExtendedLibraryConfigTestCase(unittest.TestCase): def test_extend_config_disable_rule(self): - new = config.parse_config('extends: default\n' - 'rules:\n' - ' trailing-spaces: disable\n') + old = config.YamlLintConfig('extends: default') + new = config.YamlLintConfig('extends: default\n' + 'rules:\n' + ' trailing-spaces: disable\n') - base = self.base.copy() - del base['trailing-spaces'] + old.rules['trailing-spaces'] = False - self.assertEqual(sorted(new.keys()), sorted(base.keys())) - for rule in new: - self.assertEqual(new[rule], base[rule]) + self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys())) + for rule in new.rules: + self.assertEqual(new.rules[rule], old.rules[rule]) def test_extend_config_override_whole_rule(self): - new = config.parse_config('extends: default\n' - 'rules:\n' - ' empty-lines:\n' - ' max: 42\n' - ' max-start: 43\n' - ' max-end: 44\n') - - base = self.base.copy() - base['empty-lines']['max'] = 42 - base['empty-lines']['max-start'] = 43 - base['empty-lines']['max-end'] = 44 - - self.assertEqual(sorted(new.keys()), sorted(base.keys())) - for rule in new: - self.assertEqual(new[rule], base[rule]) + old = config.YamlLintConfig('extends: default') + new = config.YamlLintConfig('extends: default\n' + 'rules:\n' + ' empty-lines:\n' + ' max: 42\n' + ' max-start: 43\n' + ' max-end: 44\n') + + old.rules['empty-lines']['max'] = 42 + old.rules['empty-lines']['max-start'] = 43 + old.rules['empty-lines']['max-end'] = 44 + + self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys())) + for rule in new.rules: + self.assertEqual(new.rules[rule], old.rules[rule]) def test_extend_config_override_rule_partly(self): - new = config.parse_config('extends: default\n' - 'rules:\n' - ' empty-lines:\n' - ' max-start: 42\n') + old = config.YamlLintConfig('extends: default') + new = config.YamlLintConfig('extends: default\n' + 'rules:\n' + ' empty-lines:\n' + ' max-start: 42\n') - base = self.base.copy() - base['empty-lines']['max-start'] = 42 + old.rules['empty-lines']['max-start'] = 42 - self.assertEqual(sorted(new.keys()), sorted(base.keys())) - for rule in new: - self.assertEqual(new[rule], base[rule]) + self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys())) + for rule in new.rules: + self.assertEqual(new.rules[rule], old.rules[rule]) diff --git a/yamllint/__init__.py b/yamllint/__init__.py index a8d2d2b..fbc3675 100644 --- a/yamllint/__init__.py +++ b/yamllint/__init__.py @@ -16,7 +16,6 @@ import yaml -from yamllint import config from yamllint.errors import LintProblem from yamllint import parser @@ -32,7 +31,7 @@ __version__ = APP_VERSION def get_costemic_problems(buffer, conf): - rules = config.get_enabled_rules(conf) + rules = conf.enabled_rules() # Split token rules from line rules token_rules = [r for r in rules if r.TYPE == 'token'] @@ -45,7 +44,7 @@ def get_costemic_problems(buffer, conf): for elem in parser.token_or_line_generator(buffer): if isinstance(elem, parser.Token): for rule in token_rules: - rule_conf = conf[rule.ID] + rule_conf = conf.rules[rule.ID] for problem in rule.check(rule_conf, elem.curr, elem.prev, elem.next, context[rule.ID]): @@ -54,7 +53,7 @@ def get_costemic_problems(buffer, conf): yield problem elif isinstance(elem, parser.Line): for rule in line_rules: - rule_conf = conf[rule.ID] + rule_conf = conf.rules[rule.ID] for problem in rule.check(rule_conf, elem): problem.rule = rule.ID problem.level = rule_conf['level'] diff --git a/yamllint/cli.py b/yamllint/cli.py index cbe3b8d..0dc8511 100644 --- a/yamllint/cli.py +++ b/yamllint/cli.py @@ -22,8 +22,7 @@ import sys import argparse from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION -from yamllint import config -from yamllint.errors import YamlLintConfigError +from yamllint.config import YamlLintConfig, YamlLintConfigError from yamllint import lint @@ -82,11 +81,11 @@ def run(argv): try: if args.config_file is not None: - conf = config.parse_config_from_file(args.config_file) + conf = YamlLintConfig(file=args.config_file) elif os.path.isfile('.yamllint'): - conf = config.parse_config_from_file('.yamllint') + conf = YamlLintConfig(file='.yamllint') else: - conf = config.parse_config('extends: default') + conf = YamlLintConfig('extends: default') except YamlLintConfigError as e: print(e, file=sys.stderr) sys.exit(-1) diff --git a/yamllint/config.py b/yamllint/config.py index fc9d4fb..0814dfd 100644 --- a/yamllint/config.py +++ b/yamllint/config.py @@ -19,95 +19,116 @@ import os.path import yaml import yamllint.rules -from yamllint.errors import YamlLintConfigError -def get_extended_conf(name): - # Is it a standard conf shipped with yamllint... - if '/' not in name: - std_conf = os.path.join(os.path.dirname(os.path.realpath(__file__)), - 'conf', name + '.yml') - - if os.path.isfile(std_conf): - return std_conf +class YamlLintConfigError(Exception): + pass - # or a custom conf on filesystem? - return name +class YamlLintConfig(object): + def __init__(self, content=None, file=None): + assert (content is None) ^ (file is None) -def extend_config(content): - try: - conf = yaml.safe_load(content) + if file is not None: + with open(file) as f: + content = f.read() - if 'rules' not in conf: - conf['rules'] = {} + self.parse(content) + self.validate() - # Does this conf override another conf that we need to load? - if 'extends' in conf: - base = parse_config_from_file(get_extended_conf(conf['extends'])) + def enabled_rules(self): + return [yamllint.rules.get(id) for id, val in self.rules.items() + if val is not False] - for rule in conf['rules']: - if type(conf['rules'][rule]) == dict and rule in base: - base[rule].update(conf['rules'][rule]) - else: - base[rule] = conf['rules'][rule] - conf['rules'] = base + def extend(self, base_config): + assert isinstance(base_config, YamlLintConfig) - return conf - except Exception as e: - raise YamlLintConfigError('invalid config: %s' % e) + for rule in self.rules: + if (type(self.rules[rule]) == dict and + rule in base_config.rules and + base_config.rules[rule] is not False): + base_config.rules[rule].update(self.rules[rule]) + else: + base_config.rules[rule] = self.rules[rule] + self.rules = base_config.rules -def parse_config(content): - conf = extend_config(content) - rules = {} - - for id in conf['rules']: + def parse(self, raw_content): try: - rule = yamllint.rules.get(id) + conf = yaml.safe_load(raw_content) except Exception as e: raise YamlLintConfigError('invalid config: %s' % e) - if conf['rules'][id] == 'disable': - continue + self.rules = conf.get('rules', {}) - rules[id] = {'level': 'error'} - if type(conf['rules'][id]) == dict: - if 'level' in conf['rules'][id]: - if conf['rules'][id]['level'] not in ('error', 'warning'): + # Does this conf override another conf that we need to load? + if 'extends' in conf: + path = get_extended_config_file(conf['extends']) + base = YamlLintConfig(file=path) + try: + self.extend(base) + except Exception as e: + raise YamlLintConfigError('invalid config: %s' % e) + + def validate(self): + for id in self.rules: + try: + rule = yamllint.rules.get(id) + except Exception as e: + raise YamlLintConfigError('invalid config: %s' % e) + + self.rules[id] = validate_rule_conf(rule, self.rules[id]) + + +def validate_rule_conf(rule, conf): + if conf is False or conf == 'disable': + return False + + if type(conf) == dict: + if 'level' not in conf: + conf['level'] = 'error' + elif conf['level'] not in ('error', 'warning'): + raise YamlLintConfigError( + 'invalid config: level should be "error" or "warning"') + + options = getattr(rule, 'CONF', {}) + for optkey in conf: + if optkey == 'level': + continue + if optkey not in options: + raise YamlLintConfigError( + 'invalid config: unknown option "%s" for rule "%s"' % + (optkey, rule.ID)) + if type(options[optkey]) == tuple: + if conf[optkey] not in options[optkey]: raise YamlLintConfigError( - 'invalid config: level should be "error" or "warning"') - rules[id]['level'] = conf['rules'][id]['level'] - - options = getattr(rule, 'CONF', {}) - for optkey in conf['rules'][id]: - if optkey == 'level': - continue - if optkey not in options: + 'invalid config: option "%s" of "%s" should be in %s' + % (optkey, rule.ID, options[optkey])) + else: + if type(conf[optkey]) != options[optkey]: raise YamlLintConfigError( - 'invalid config: unknown option "%s" for rule "%s"' % - (optkey, id)) - if type(options[optkey]) == tuple: - if conf['rules'][id][optkey] not in options[optkey]: - raise YamlLintConfigError( - ('invalid config: option "%s" of "%s" should be ' - 'in %s') % (optkey, id, options[optkey])) - else: - if type(conf['rules'][id][optkey]) != options[optkey]: - raise YamlLintConfigError( - ('invalid config: option "%s" of "%s" should be ' - '%s' % (optkey, id, options[optkey].__name__))) - rules[id][optkey] = conf['rules'][id][optkey] - else: - raise YamlLintConfigError(('invalid config: rule "%s": should be ' - 'either "disable" or a dict') % id) - return rules - - -def parse_config_from_file(path): - with open(path) as f: - return parse_config(f.read()) - - -def get_enabled_rules(conf): - return [yamllint.rules.get(r) for r in conf.keys() if conf[r] is not False] + 'invalid config: option "%s" of "%s" should be %s' + % (optkey, rule.ID, options[optkey].__name__)) + for optkey in options: + if optkey not in conf: + raise YamlLintConfigError( + 'invalid config: missing option "%s" for rule "%s"' % + (optkey, rule.ID)) + else: + raise YamlLintConfigError(('invalid config: rule "%s": should be ' + 'either "disable" or a dict') % rule.ID) + + return conf + + +def get_extended_config_file(name): + # Is it a standard conf shipped with yamllint... + if '/' not in name: + std_conf = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'conf', name + '.yml') + + if os.path.isfile(std_conf): + return std_conf + + # or a custom conf on filesystem? + return name