From 1eef6f4d61153ab4947816853ec638ef81c74743 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sat, 27 Dec 2014 13:11:21 +0100 Subject: [PATCH] New question file format support --- game.py | 2 +- question.py | 114 ++++++++++++++++++++++++++++++++++++++++------------ seopardy.py | 3 +- 3 files changed, 91 insertions(+), 28 deletions(-) diff --git a/game.py b/game.py index 2a09bdb..774fdea 100644 --- a/game.py +++ b/game.py @@ -42,7 +42,7 @@ class SeopardyGame(QtGui.QWidget): layout = QtGui.QVBoxLayout() headerLayout = QtGui.QHBoxLayout() - self.header = QtGui.QLabel("") + self.header = QtGui.QLabel(self.questions.get_title()) headerLayout.addWidget(self.header) headerLayout.setSizeConstraint(QtGui.QLayout.SetMinimumSize) layout.addWidget(self.header, alignment=QtCore.Qt.AlignCenter) diff --git a/question.py b/question.py index 7e4dd2e..dab5d0e 100644 --- a/question.py +++ b/question.py @@ -13,12 +13,17 @@ class Questions(object): QUESTION_TYPES = ["Text", "Image", "Music", "Code", "Video"] QUESTION_KEYS = ["Name", "Question", "Answer", "Type", "Double-Jeopardy", "Audio"] - def __init__(self, qfile, appendPath=False): + def __init__(self, qfile, appendPath=False, verbose=False): self.qfile = qfile - self._questions = None + self._questions = [] self._appendPath = appendPath - self._basedir = os.path.dirname(qfile) - self._read_questions() + self._verbose = verbose + self._title = "Round n" + + self._read_questions(self.qfile) + + def get_title(self): + return self._title def get_sections(self): return [s["Section"] for s in self._questions] @@ -40,80 +45,137 @@ class Questions(object): return i raise ValueError("Section '%s' does not exist" % section) - def _read_questions(self): + def _get_yaml(self, filename): f = None # open file try: - f = open(self.qfile) + f = open(filename) except OSError as e: - raise QuestionException("Could not read question file: %s" % e) + raise QuestionException("Could not read question file '%s': %s" % (filename, e)) # load yaml ysrc = None try: ysrc = yaml.safe_load(f.read()) except Exception as e: - raise QuestionException("Error parsing question file %s: %s" % (self.qfile, e)) + raise QuestionException("Error parsing YAML in question file '%s': %s" % (filename, e)) + + return ysrc + + def _gen_error(self, filename, error): + raise QuestionException("Error parsing question file '%s': %s" % (filename, error)) + + def _read_questions(self, filename): + ysrc = self._get_yaml(filename) + + if type(ysrc) == list: + # "old style" question file, list of sections + # just read sections and return happily ever after + self._read_sections(ysrc, filename) + + return True + elif type(ysrc) == dict: + # "new style" quetion file, dict of Name and Sections + self._read_newstyle_questions(ysrc, filename) + + return True + else: + self._gen_error(filename, "Contents of YAML is neither a dict nor a list (format error), see seopardy guide for question file format documentation.") + + def _read_newstyle_questions(self, ysrc, filename): + if type(ysrc) != dict: + self._gen_error(filename, "Error parsing question file '%s': Is not a dict (and this sould(tm) have been safeguarded by an earlier if)") + + if "Name" not in ysrc.keys(): + self._gen_error(filename, "Missing 'Name' key.") + + if "Sections" not in ysrc.keys(): + self._gen_error(filename, "Missing 'Sections' key.") + + extra_keys = filter(lambda _x: _x not in ["Name", "Sections"], ysrc.keys()) + if extra_keys: + self._gen_error(filename, "Unsupported keys found: %s (only 'Name' and 'Sections' are supported. Is your case correct?)" % (", ".join(extra_keys),)) + + if type(ysrc["Sections"]) != list: + self._gen_error(filename, "The 'Sections' part needs to be a list of filenames, not a '%s'" % (type(ysrc["Sections"]),)) + + self._title = ysrc["Name"] + + # read sections + basedir = os.path.dirname(filename) + for n, sec_filename in enumerate(ysrc["Sections"], 1): + if type(sec_filename) != str: + self._gen_error(filename, "Section element %d is not a string (type %s found)" % (n, type(sec_filename))) + + fpath = os.path.join(basedir, sec_filename) + sec_ysrc = self._get_yaml(fpath) + self._read_sections(sec_ysrc, fpath) + + def _read_sections(self, ysrc, filename): + basedir = os.path.dirname(filename) # now to check the integrity of the question file if type(ysrc) is not list: - raise QuestionException("The questionfile has to be a list of question") + self._gen_error(filename, "The questionfile has to be a list of sections") for i, sec in enumerate(ysrc, 1): if not "Section" in sec.keys() or not "Questions" in sec.keys(): - raise QuestionException("Section %d needs to have the keys 'Section' and 'Question' (case-sensitive)" % i) + self._gen_error(filename, "Section %d needs to have the keys 'Section' and 'Question' (case-sensitive)" % i) for j, q in enumerate(sec["Questions"], 1): # check for keys we need in each question if any([x not in q.keys() for x in ["Question", "Answer", "Type"]]): - raise QuestionException("Question %d from section %d (%s) is missing one of the keywords Question, Answer or Type" % (j, i, sec["Section"])) + self._gen_error(filename, "Question %d from section %d (%s) is missing one of the keywords Question, Answer or Type" % (j, i, sec["Section"])) # check wether the question is a string (else we'll get display errors) if type(q["Question"]) != str: - raise QuestionException("Question %d from section %d (%s) needs to have a string as question (put the Question in \"\")" % (j, i, sec["Section"])) - print(type(q["Question"])) + self._gen_error(filename, "Question %d from section %d (%s) needs to have a string as question (put the Question in \"\")" % (j, i, sec["Section"])) # check for keys we do not know for key in q.keys(): if key not in self.QUESTION_KEYS: - raise QuestionException("Qestion %d from section %d (%s) has invalid keyword '%s'" % (j, i, sec["Section"], key)) + self._gen_error(filename, "Qestion %d from section %d (%s) has invalid keyword '%s'" % (j, i, sec["Section"], key)) # check Double-Jeopardy is a bool and is set to false if non-existant if "Double-Jeopardy" not in q.keys(): q["Double-Jeopardy"] = False elif type(q["Double-Jeopardy"]) != bool: - raise QuestionException("The Double-Jeopardy key from question %d from section %d (%s) must be either true or false" % (j, i, sec["Section"])) + self._gen_error(filename, "The Double-Jeopardy key from question %d from section %d (%s) must be either true or false" % (j, i, sec["Section"])) # check Audio is a bool and is set to false if non-existant if "Audio" not in q.keys(): q["Audio"] = False elif type(q["Audio"]) != bool: - raise QuestionException("The Audio key from question %d from section %d (%s) must be either true or false" % (j, i, sec["Section"])) + self._gen_error(filename, "The Audio key from question %d from section %d (%s) must be either true or false" % (j, i, sec["Section"])) # check for broken question types if q["Type"] not in self.QUESTION_TYPES: - raise QuestionException("Question %d from Section %d (%s) has an invalid type '%s' (valid types are %s)" % (j, i, sec["Section"], q["Type"], ", ".join(self.QUESTION_TYPES))) + self._gen_error(filename, "Question %d from Section %d (%s) has an invalid type '%s' (valid types are %s)" % (j, i, sec["Section"], q["Type"], ", ".join(self.QUESTION_TYPES))) # check if file for music/image questions exist if q["Type"] in ("Music", "Image", "Video"): if self._appendPath: - q["Question"] = os.path.join(self._basedir, q["Question"]) + q["Question"] = os.path.join(basedir, q["Question"]) if not os.path.isfile(q["Question"]): - raise QuestionException("File for question %d, section %d (%s) not found" % (j, i, sec["Section"])) + fname_error = "" + if self._verbose: + fname_error = " (path: %s)" % (q["Question"],) + self._gen_error(filename, "File for question %d, section %d (%s) not found%s" % (j, i, sec["Section"], fname_error)) # check if this section has enough questions if j != 5: - raise QuestionException("Section %d (%s) needs to have exactly %d questions (has %d)" % (i, sec["Section"], 5, j)) - - # check for only having unique section names - sections = [s["Section"] for s in ysrc] - if len(sections) != len(set(sections)): - raise QuestionException("All section names must be unique") + self._gen_error(filename, "Section %d (%s) needs to have exactly %d questions (has %d)" % (i, sec["Section"], 5, j)) # done, save yaml src to _questions - self._questions = ysrc + self._questions.extend(ysrc) + + # check for only having unique section names + sections = [s["Section"] for s in self._questions] + if len(sections) != len(set(sections)): + map(lambda _x: sections.remove(_x), set(sections)) + self._gen_error(filename, "All section names must be unique (clashing: %s)" % (", ".join(sections),)) return True diff --git a/seopardy.py b/seopardy.py index f8422d1..809573b 100755 --- a/seopardy.py +++ b/seopardy.py @@ -18,6 +18,7 @@ def _parser(): parser.add_argument("--gamestate", action="store", default=None, help="Gamestate to load to recover a crashed game") parser.add_argument("--conf", action="store", default="seopardy.conf", help="Path to config file") parser.add_argument("--no-cwd", action="store_true", default=False, help="Do not change working directory to question folder") + parser.add_argument("-v", "--verbose", action="store_true", default=False, help="Be more verbose, print filenames of missing files") parser.add_argument("questions", action="store", help="Path to questionfile") return parser @@ -28,7 +29,7 @@ if __name__ == '__main__': questions = None try: - questions = Questions(args.questions, appendPath=not args.no_cwd) + questions = Questions(args.questions, appendPath=not args.no_cwd, verbose=args.verbose) except QuestionException as e: print(str(e), file=sys.stderr) sys.exit(1)