Config: Refactor to use YamlLintConfig objects

pull/4/head
Adrien Vergé 9 years ago
parent 792bdf99b4
commit fc108e7cee

@ -18,7 +18,7 @@ import unittest
import yaml import yaml
from yamllint.config import parse_config from yamllint.config import YamlLintConfig
from yamllint.errors import LintProblem from yamllint.errors import LintProblem
from yamllint import lint from yamllint import lint
@ -31,7 +31,7 @@ class RuleTestCase(unittest.TestCase):
conf = yaml.safe_load(conf) conf = yaml.safe_load(conf)
conf = {'extends': 'default', conf = {'extends': 'default',
'rules': conf} 'rules': conf}
return parse_config(yaml.safe_dump(conf)) return YamlLintConfig(yaml.safe_dump(conf))
def check(self, source, conf, **kwargs): def check(self, source, conf, **kwargs):
expected_problems = [] expected_problems = []

@ -14,56 +14,164 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import unittest import unittest
from yamllint import config from yamllint import config
class ConfigTestCase(unittest.TestCase): class SimpleConfigTestCase(unittest.TestCase):
def setUp(self): def test_parse_config(self):
self.base = config.parse_config_from_file(os.path.join( new = config.YamlLintConfig('rules:\n'
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), ' colons:\n'
'yamllint', 'conf', 'default.yml')) ' 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): def test_extend_config_disable_rule(self):
new = config.parse_config('extends: default\n' old = config.YamlLintConfig('extends: default')
'rules:\n' new = config.YamlLintConfig('extends: default\n'
' trailing-spaces: disable\n') 'rules:\n'
' trailing-spaces: disable\n')
base = self.base.copy() old.rules['trailing-spaces'] = False
del base['trailing-spaces']
self.assertEqual(sorted(new.keys()), sorted(base.keys())) self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys()))
for rule in new: for rule in new.rules:
self.assertEqual(new[rule], base[rule]) self.assertEqual(new.rules[rule], old.rules[rule])
def test_extend_config_override_whole_rule(self): def test_extend_config_override_whole_rule(self):
new = config.parse_config('extends: default\n' old = config.YamlLintConfig('extends: default')
'rules:\n' new = config.YamlLintConfig('extends: default\n'
' empty-lines:\n' 'rules:\n'
' max: 42\n' ' empty-lines:\n'
' max-start: 43\n' ' max: 42\n'
' max-end: 44\n') ' max-start: 43\n'
' max-end: 44\n')
base = self.base.copy()
base['empty-lines']['max'] = 42 old.rules['empty-lines']['max'] = 42
base['empty-lines']['max-start'] = 43 old.rules['empty-lines']['max-start'] = 43
base['empty-lines']['max-end'] = 44 old.rules['empty-lines']['max-end'] = 44
self.assertEqual(sorted(new.keys()), sorted(base.keys())) self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys()))
for rule in new: for rule in new.rules:
self.assertEqual(new[rule], base[rule]) self.assertEqual(new.rules[rule], old.rules[rule])
def test_extend_config_override_rule_partly(self): def test_extend_config_override_rule_partly(self):
new = config.parse_config('extends: default\n' old = config.YamlLintConfig('extends: default')
'rules:\n' new = config.YamlLintConfig('extends: default\n'
' empty-lines:\n' 'rules:\n'
' max-start: 42\n') ' empty-lines:\n'
' max-start: 42\n')
base = self.base.copy() old.rules['empty-lines']['max-start'] = 42
base['empty-lines']['max-start'] = 42
self.assertEqual(sorted(new.keys()), sorted(base.keys())) self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys()))
for rule in new: for rule in new.rules:
self.assertEqual(new[rule], base[rule]) self.assertEqual(new.rules[rule], old.rules[rule])

@ -16,7 +16,6 @@
import yaml import yaml
from yamllint import config
from yamllint.errors import LintProblem from yamllint.errors import LintProblem
from yamllint import parser from yamllint import parser
@ -32,7 +31,7 @@ __version__ = APP_VERSION
def get_costemic_problems(buffer, conf): def get_costemic_problems(buffer, conf):
rules = config.get_enabled_rules(conf) rules = conf.enabled_rules()
# Split token rules from line rules # Split token rules from line rules
token_rules = [r for r in rules if r.TYPE == 'token'] 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): for elem in parser.token_or_line_generator(buffer):
if isinstance(elem, parser.Token): if isinstance(elem, parser.Token):
for rule in token_rules: for rule in token_rules:
rule_conf = conf[rule.ID] rule_conf = conf.rules[rule.ID]
for problem in rule.check(rule_conf, for problem in rule.check(rule_conf,
elem.curr, elem.prev, elem.next, elem.curr, elem.prev, elem.next,
context[rule.ID]): context[rule.ID]):
@ -54,7 +53,7 @@ def get_costemic_problems(buffer, conf):
yield problem yield problem
elif isinstance(elem, parser.Line): elif isinstance(elem, parser.Line):
for rule in line_rules: for rule in line_rules:
rule_conf = conf[rule.ID] rule_conf = conf.rules[rule.ID]
for problem in rule.check(rule_conf, elem): for problem in rule.check(rule_conf, elem):
problem.rule = rule.ID problem.rule = rule.ID
problem.level = rule_conf['level'] problem.level = rule_conf['level']

@ -22,8 +22,7 @@ import sys
import argparse import argparse
from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION from yamllint import APP_DESCRIPTION, APP_NAME, APP_VERSION
from yamllint import config from yamllint.config import YamlLintConfig, YamlLintConfigError
from yamllint.errors import YamlLintConfigError
from yamllint import lint from yamllint import lint
@ -82,11 +81,11 @@ def run(argv):
try: try:
if args.config_file is not None: 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'): elif os.path.isfile('.yamllint'):
conf = config.parse_config_from_file('.yamllint') conf = YamlLintConfig(file='.yamllint')
else: else:
conf = config.parse_config('extends: default') conf = YamlLintConfig('extends: default')
except YamlLintConfigError as e: except YamlLintConfigError as e:
print(e, file=sys.stderr) print(e, file=sys.stderr)
sys.exit(-1) sys.exit(-1)

@ -19,95 +19,116 @@ import os.path
import yaml import yaml
import yamllint.rules import yamllint.rules
from yamllint.errors import YamlLintConfigError
def get_extended_conf(name): class YamlLintConfigError(Exception):
# Is it a standard conf shipped with yamllint... pass
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
class YamlLintConfig(object):
def __init__(self, content=None, file=None):
assert (content is None) ^ (file is None)
def extend_config(content): if file is not None:
try: with open(file) as f:
conf = yaml.safe_load(content) content = f.read()
if 'rules' not in conf: self.parse(content)
conf['rules'] = {} self.validate()
# Does this conf override another conf that we need to load? def enabled_rules(self):
if 'extends' in conf: return [yamllint.rules.get(id) for id, val in self.rules.items()
base = parse_config_from_file(get_extended_conf(conf['extends'])) if val is not False]
for rule in conf['rules']: def extend(self, base_config):
if type(conf['rules'][rule]) == dict and rule in base: assert isinstance(base_config, YamlLintConfig)
base[rule].update(conf['rules'][rule])
else:
base[rule] = conf['rules'][rule]
conf['rules'] = base
return conf for rule in self.rules:
except Exception as e: if (type(self.rules[rule]) == dict and
raise YamlLintConfigError('invalid config: %s' % e) 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): def parse(self, raw_content):
conf = extend_config(content)
rules = {}
for id in conf['rules']:
try: try:
rule = yamllint.rules.get(id) conf = yaml.safe_load(raw_content)
except Exception as e: except Exception as e:
raise YamlLintConfigError('invalid config: %s' % e) raise YamlLintConfigError('invalid config: %s' % e)
if conf['rules'][id] == 'disable': self.rules = conf.get('rules', {})
continue
rules[id] = {'level': 'error'} # Does this conf override another conf that we need to load?
if type(conf['rules'][id]) == dict: if 'extends' in conf:
if 'level' in conf['rules'][id]: path = get_extended_config_file(conf['extends'])
if conf['rules'][id]['level'] not in ('error', 'warning'): 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( raise YamlLintConfigError(
'invalid config: level should be "error" or "warning"') 'invalid config: option "%s" of "%s" should be in %s'
rules[id]['level'] = conf['rules'][id]['level'] % (optkey, rule.ID, options[optkey]))
else:
options = getattr(rule, 'CONF', {}) if type(conf[optkey]) != options[optkey]:
for optkey in conf['rules'][id]:
if optkey == 'level':
continue
if optkey not in options:
raise YamlLintConfigError( raise YamlLintConfigError(
'invalid config: unknown option "%s" for rule "%s"' % 'invalid config: option "%s" of "%s" should be %s'
(optkey, id)) % (optkey, rule.ID, options[optkey].__name__))
if type(options[optkey]) == tuple: for optkey in options:
if conf['rules'][id][optkey] not in options[optkey]: if optkey not in conf:
raise YamlLintConfigError( raise YamlLintConfigError(
('invalid config: option "%s" of "%s" should be ' 'invalid config: missing option "%s" for rule "%s"' %
'in %s') % (optkey, id, options[optkey])) (optkey, rule.ID))
else: else:
if type(conf['rules'][id][optkey]) != options[optkey]: raise YamlLintConfigError(('invalid config: rule "%s": should be '
raise YamlLintConfigError( 'either "disable" or a dict') % rule.ID)
('invalid config: option "%s" of "%s" should be '
'%s' % (optkey, id, options[optkey].__name__))) return conf
rules[id][optkey] = conf['rules'][id][optkey]
else:
raise YamlLintConfigError(('invalid config: rule "%s": should be ' def get_extended_config_file(name):
'either "disable" or a dict') % id) # Is it a standard conf shipped with yamllint...
return rules if '/' not in name:
std_conf = os.path.join(os.path.dirname(os.path.realpath(__file__)),
'conf', name + '.yml')
def parse_config_from_file(path):
with open(path) as f: if os.path.isfile(std_conf):
return parse_config(f.read()) return std_conf
# or a custom conf on filesystem?
def get_enabled_rules(conf): return name
return [yamllint.rules.get(r) for r in conf.keys() if conf[r] is not False]

Loading…
Cancel
Save