Fixed linting issues with validator

Fleshed out validator output
This commit is contained in:
John Donaldson 2019-07-17 23:40:28 +01:00 committed by Donaldson
parent 5cdfc5e852
commit ccf9461d28
1 changed files with 73 additions and 28 deletions

View File

@ -1,9 +1,15 @@
#!/usr/bin/python3 #!/usr/bin/python3
"""A validator for MOTH puzzles"""
import logging import logging
import os
import os.path
import moth import moth
# pylint: disable=len-as-condition, line-too-long
DEFAULT_REQUIRED_FIELDS = ["answers", "authors", "summary"] DEFAULT_REQUIRED_FIELDS = ["answers", "authors", "summary"]
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -11,91 +17,125 @@ LOGGER = logging.getLogger(__name__)
class MothValidationError(Exception): class MothValidationError(Exception):
pass """An exception for encapsulating MOTH puzzle validation errors"""
class MothValidator: class MothValidator:
"""A class which validates MOTH categories"""
def __init__(self, fields): def __init__(self, fields):
self.required_fields = fields self.required_fields = fields
self.results = {} self.results = {"category": {}, "checks": []}
def validate(self, categorydir): def validate(self, categorydir, only_errors=False):
"""Run validation checks against a category"""
LOGGER.debug("Loading category from %s", categorydir) LOGGER.debug("Loading category from %s", categorydir)
category = moth.Category(categorydir, 0) category = moth.Category(categorydir, 0)
LOGGER.debug("Found %d puzzles in %s", len(category.pointvals()), categorydir) LOGGER.debug("Found %d puzzles in %s", len(category.pointvals()), categorydir)
self.results[categorydir] = {} self.results["category"][categorydir] = {
curr_category = self.results[categorydir] "puzzles": {},
"name": os.path.basename(categorydir.strip(os.sep)),
}
curr_category = self.results["category"][categorydir]
for check_function_name in [x for x in dir(self) if x.startswith("check_") and callable(getattr(self, x))]:
if check_function_name not in self.results["checks"]:
self.results["checks"].append(check_function_name)
for puzzle in category: for puzzle in category:
LOGGER.info("Processing %s: %s", categorydir, puzzle.points) LOGGER.info("Processing %s: %s", categorydir, puzzle.points)
curr_category[puzzle.points] = {} curr_category["puzzles"][puzzle.points] = {}
curr_puzzle = curr_category[puzzle.points] curr_puzzle = curr_category["puzzles"][puzzle.points]
curr_puzzle["checks"] = []
curr_puzzle["failures"] = [] curr_puzzle["failures"] = []
for check_function_name in [x for x in dir(self) if x.startswith("check_") and callable(getattr(self, x))]: for check_function_name in [x for x in dir(self) if x.startswith("check_") and callable(getattr(self, x))]:
check_function = getattr(self, check_function_name) check_function = getattr(self, check_function_name)
LOGGER.debug("Running %s on %d", check_function_name, puzzle.points) LOGGER.debug("Running %s on %d", check_function_name, puzzle.points)
curr_puzzle["checks"].append(check_function_name)
try: try:
check_function(puzzle) check_function(puzzle)
except MothValidationError as ex: except MothValidationError as ex:
curr_puzzle["failures"].append(ex) curr_puzzle["failures"].append(str(ex))
LOGGER.exception(ex)
if only_errors and len(curr_puzzle["failures"]) == 0:
del curr_category["puzzles"][puzzle.points]
def check_fields(self, puzzle): def check_fields(self, puzzle):
"""Check if the puzzle has the requested fields"""
for field in self.required_fields: for field in self.required_fields:
if not hasattr(puzzle, field): if not hasattr(puzzle, field):
raise MothValidationError("Missing field %s" % (field,)) raise MothValidationError("Missing field %s" % (field,))
def check_has_answers(self, puzzle): @staticmethod
def check_has_answers(puzzle):
"""Check if the puzle has answers defined"""
if len(puzzle.answers) == 0: if len(puzzle.answers) == 0:
raise MothValidationError("No answers provided") raise MothValidationError("No answers provided")
def check_has_authors(self, puzzle): @staticmethod
def check_has_authors(puzzle):
"""Check if the puzzle has authors defined"""
if len(puzzle.authors) == 0: if len(puzzle.authors) == 0:
raise MothValidationError("No authors provided") raise MothValidationError("No authors provided")
def check_has_summary(self, puzzle): @staticmethod
def check_has_summary(puzzle):
"""Check if the puzzle has a summary"""
if puzzle.summary is None: if puzzle.summary is None:
raise MothValidationError("Summary has not been provided") raise MothValidationError("Summary has not been provided")
def check_has_body(self, puzzle): @staticmethod
def check_has_body(puzzle):
"""Check if the puzzle has a body defined"""
old_pos = puzzle.body.tell() old_pos = puzzle.body.tell()
puzzle.body.seek(0) puzzle.body.seek(0)
if len(puzzle.body.read()) == 0: if len(puzzle.body.read()) == 0:
puzzle.body.seek(old_pos) puzzle.body.seek(old_pos)
raise MothValidationError("No body provided") raise MothValidationError("No body provided")
else:
puzzle.body.seek(old_pos) puzzle.body.seek(old_pos)
# Leaving this as a placeholder until KSAs are formally supported # Leaving this as a placeholder until KSAs are formally supported
def check_ksa_format(self, puzzle): @staticmethod
def check_ksa_format(puzzle):
"""Check if KSAs are properly formatted"""
if hasattr(puzzle, "ksa"): if hasattr(puzzle, "ksa"):
for ksa in puzzle.ksa: for ksa in puzzle.ksa:
if not ksa.startswith("K"): if not ksa.startswith("K"):
raise MothValidationError("Unrecognized KSA format") raise MothValidationError("Unrecognized KSA format")
def output_json(data): def output_json(data):
"""Output results in JSON format"""
import json import json
print(json.dumps(data)) print(json.dumps(data))
def output_text(data): def output_text(data):
for category, cat_data in data.items(): """Output results in a text-based tabular format"""
print("= %s =" % (category,))
print("| Points | Checks | Errors |") longest_category = max([len(y["name"]) for x, y in data["category"].items()])
for points, puzzle_data in cat_data.items(): longest_category = max([longest_category, len("Category")])
print("| %d | %s | %s |" % (points, ", ".join(puzzle_data["checks"]), puzzle_data["failures"])) longest_failure = len("Failures")
for category_data in data["category"].values():
for points, puzzle_data in category_data["puzzles"].items():
longest_failure = max([longest_failure, len(", ".join(puzzle_data["failures"]))])
formatstr = "| %%%ds | %%6s | %%%ds |" % (longest_category, longest_failure)
headerfmt = formatstr % ("Category", "Points", "Failures")
print(headerfmt)
for cat_data in data["category"].values():
for points, puzzle_data in sorted(cat_data["puzzles"].items()):
print(formatstr % (cat_data["name"], points, ", ".join([str(x) for x in puzzle_data["failures"]])))
if __name__ == "__main__": def main():
"""Main function"""
# pylint: disable=invalid-name
import argparse import argparse
LOGGER.addHandler(logging.StreamHandler()) LOGGER.addHandler(logging.StreamHandler())
@ -104,7 +144,8 @@ if __name__ == "__main__":
parser.add_argument("category", nargs="+", help="Categories to validate") parser.add_argument("category", nargs="+", help="Categories to validate")
parser.add_argument("-f", "--fields", help="Comma-separated list of fields to check for", default=",".join(DEFAULT_REQUIRED_FIELDS)) parser.add_argument("-f", "--fields", help="Comma-separated list of fields to check for", default=",".join(DEFAULT_REQUIRED_FIELDS))
parser.add_argument("-o", "--output-format", choices=["text", "json", "csv"], default="text", help="Output format (default: text)") parser.add_argument("-o", "--output-format", choices=["text", "json"], default="text", help="Output format (default: text)")
parser.add_argument("-e", "--only-errors", action="store_true", default=False, help="Only output errors")
parser.add_argument("-v", "--verbose", action="count", default=0, help="Increase verbosity of output, repeat to increase") parser.add_argument("-v", "--verbose", action="count", default=0, help="Increase verbosity of output, repeat to increase")
args = parser.parse_args() args = parser.parse_args()
@ -119,9 +160,13 @@ if __name__ == "__main__":
for category in args.category: for category in args.category:
LOGGER.info("Validating %s", category) LOGGER.info("Validating %s", category)
validator.validate(category) validator.validate(category, only_errors=args.only_errors)
if args.output_format == "text": if args.output_format == "text":
output_text(validator.results) output_text(validator.results)
elif args.output_format == "json": elif args.output_format == "json":
output_json(validator.results) output_json(validator.results)
if __name__ == "__main__":
main()