Skip to content

Commit

Permalink
Added new feature that can make validation of schema easier by doing …
Browse files Browse the repository at this point in the history
…a validation of what keywords is applicable to each defined "type"

This feature is opt-in for now and will be mandatory in future releases >= 1.7.0
The mapping of all valid keywords is not finished and requires more work before considered working/fully implemented feature
  • Loading branch information
Grokzen committed Dec 22, 2016
1 parent 5c2782c commit 60fcfea
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 5 deletions.
3 changes: 3 additions & 0 deletions docs/release-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ Release Notes
- Added support for new keyword "allownone" that can be used to specify that types can have None/null values at the same time as their normal defined type.
- Removed the force of UTF-8 encoding when importing pykwalify package. It caused issues with jypiter notebooks (py 2.7)
Added documentation in Readme regarding the suggested solution to use "PYTHONIOENCODING=UTF-8" if the default solution do not work.
- Added new cli flag "--strict-rule-validation" that will validate that all used keywords in all Rule objects only uses the rules that is supported by the defined type.
If you only use a Core object then set "strict_rule_validation=True" when creating the Core object instance.
This feature is opt-in in this releaes but will be mandatory in releases >= 1.7.0.


1.5.2 (Nov 12, 2016)
Expand Down
5 changes: 4 additions & 1 deletion pykwalify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ def parse_cli():
#

__docopt__ = """
usage: pykwalify -d FILE -s FILE ... [-e FILE ...] [-v ...] [-q]
usage: pykwalify -d FILE -s FILE ... [-e FILE ...] [--strict-rule-validation] [-v ...] [-q]
optional arguments:
-d FILE, --data-file FILE the file to be tested
-e FILE, --extension FILE file containing python extension
-h, --help show this help message and exit
-q, --quiet suppress terminal output
-s FILE, --schema-file FILE schema definition file
--strict-rule-validation enables strict validation of all keywords for all
Rule objects to find unsupported keyword usage
-v, --verbose verbose terminal output (multiple -v increases verbosity)
--version display the version number and exit
"""
Expand Down Expand Up @@ -67,6 +69,7 @@ def run(cli_args):
source_file=cli_args["--data-file"],
schema_files=cli_args["--schema-file"],
extensions=cli_args['--extension'],
strict_rule_validation=cli_args['--strict-rule-validation'],
)
c.validate()
return c
Expand Down
6 changes: 5 additions & 1 deletion pykwalify/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
class Core(object):
""" Core class of pyKwalify """

def __init__(self, source_file=None, schema_files=None, source_data=None, schema_data=None, extensions=None):
def __init__(self, source_file=None, schema_files=None, source_data=None, schema_data=None, extensions=None, strict_rule_validation=False):
"""
:param extensions:
List of paths to python files that should be imported and available via 'func' keywork.
Expand All @@ -53,6 +53,7 @@ def __init__(self, source_file=None, schema_files=None, source_data=None, schema
self.root_rule = None
self.extensions = extensions
self.errors = []
self.strict_rule_validation = strict_rule_validation

if source_file is not None:
if not os.path.exists(source_file):
Expand Down Expand Up @@ -126,6 +127,9 @@ def __init__(self, source_file=None, schema_files=None, source_data=None, schema

self._load_extensions()

if self.strict_rule_validation:
log.info("Using strict rule keywords validation...")

def _load_extensions(self):
"""
Load all extension files into the namespace pykwalify.ext
Expand Down
47 changes: 46 additions & 1 deletion pykwalify/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
class Rule(object):
""" Rule class that handles a rule constraint """

def __init__(self, schema=None, parent=None):
def __init__(self, schema=None, parent=None, strict_rule_validation=False):
self._allowempty_map = None
self._allownone = None
self._assertion = None
Expand All @@ -52,6 +52,7 @@ def __init__(self, schema=None, parent=None):
self._schema = schema
self._schema_str = schema
self._sequence = None
self.strict_rule_validation = strict_rule_validation
self._type = None
self._type_class = None
self._unique = None
Expand Down Expand Up @@ -430,6 +431,8 @@ def init(self, schema, path):

self.check_conflicts(schema, rule, path)

self.check_type_keywords(schema, rule, path)

def init_allownone(self, v, rule, path):
"""
"""
Expand Down Expand Up @@ -955,6 +958,48 @@ def init_default_value(self, v, rule, path):
path=path,
)

def check_type_keywords(self, schema, rule, path):
"""
"""
if not self.strict_rule_validation:
return

global_keywords = ['type', 'example', 'name', 'version']
all_allowed_keywords = {
'str': global_keywords + ['pattern', 'range', 'enum', 'required', 'unique', 'req', 'allownone'],
'int': global_keywords + ['range', 'enum', 'required', 'unique'],
'float': global_keywords + ['range', 'required'],
'number': global_keywords + [],
'bool': global_keywords + ['enum'],
'map': global_keywords + ['mapping', 'map', 'allowempty', 'required', 'matching-rule', 'range'],
'seq': global_keywords + ['sequence', 'seq', 'required', 'range', 'matching'],
'sequence': global_keywords + ['sequence', 'seq', 'required'],
'mapping': global_keywords + ['mapping', 'seq', 'required'],
'timestamp': global_keywords + [],
'date': global_keywords + [],
'symbol': global_keywords + [],
'scalar': global_keywords + [],
'text': global_keywords + ['pattern'],
'any': global_keywords + [],
'enum': global_keywords + [],
'none': global_keywords + ['required'],
}
rule_type = schema.get('type', None)
if not rule_type:
# Special cases for the "shortcut methods"
if 'sequence' in schema or 'seq' in schema:
rule_type = 'sequence'
elif 'mapping' in schema or 'map' in schema:
rule_type = 'mapping'

allowed_keywords = all_allowed_keywords.get(rule_type, None)
if not allowed_keywords and 'sequence' not in schema and 'mapping' not in schema and 'seq' not in schema and 'map' not in schema:
raise RuleError('No allowed keywords found for type: {0}'.format(rule_type))

for k, v in schema.items():
if k not in allowed_keywords:
raise RuleError('Keyword "{0}" is not supported for type: "{1}" '.format(k, rule_type))

def check_conflicts(self, schema, rule, path):
log.debug(u"Checking for conflicts : %s", path)

Expand Down
4 changes: 2 additions & 2 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ def test_core_files(self):

try:
print("Running test files: {0}".format(f))
c = Core(source_data=data, schema_data=schema)
c = Core(source_data=data, schema_data=schema, strict_rule_validation=True)
c.validate()
compare(c.validation_errors, [], prefix="No validation errors should exist...")
except Exception as e:
Expand All @@ -477,7 +477,7 @@ def test_core_files(self):

try:
print("Running test files: {0}".format(f))
c = Core(source_data=data, schema_data=schema)
c = Core(source_data=data, schema_data=schema, strict_rule_validation=True)
c.validate()
except exception_type:
pass # OK
Expand Down

0 comments on commit 60fcfea

Please sign in to comment.