diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000..d452e20 --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,3 @@ +Neale Pickett +Patrick Avery +Shannon Steinfadt diff --git a/devel-server.py b/devel-server.py index e280ffb..5543f83 100755 --- a/devel-server.py +++ b/devel-server.py @@ -1,5 +1,6 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 +import cgi import glob import http.server import mistune @@ -7,6 +8,8 @@ import os import pathlib import puzzles import socketserver +import sys +import traceback try: from http.server import HTTPStatus @@ -14,7 +17,10 @@ except ImportError: class HTTPStatus: NOT_FOUND = 404 OK = 200 - + +# XXX: This will eventually cause a problem. Do something more clever here. +seed = 1 + def page(title, body): return """ @@ -45,7 +51,20 @@ class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer): pass -class MothHandler(http.server.CGIHTTPRequestHandler): +class MothHandler(http.server.SimpleHTTPRequestHandler): + def handle_one_request(self): + try: + super().handle_one_request() + except: + tbtype, value, tb = sys.exc_info() + tblist = traceback.format_tb(tb, None) + traceback.format_exception_only(tbtype, value) + page = ("# Traceback (most recent call last)\n" + + " " + + " ".join(tblist[:-1]) + + tblist[-1]) + self.serve_md(page) + + def do_GET(self): if self.path == "/": self.serve_front() @@ -91,25 +110,22 @@ you are a fool. elif len(parts) == 3: # List all point values in a category body.append("# Puzzles in category `{}`".format(parts[2])) - puzz = [] - for i in glob.glob(os.path.join("puzzles", parts[2], "*.moth")): - base = os.path.basename(i) - root, _ = os.path.splitext(base) - points = int(root) - puzz.append(points) - for puzzle in sorted(puzz): - body.append("* [puzzles/{cat}/{points}](/puzzles/{cat}/{points}/)".format(cat=parts[2], points=puzzle)) + fpath = os.path.join("puzzles", parts[2]) + cat = puzzles.Category(fpath, seed) + for points in cat.pointvals: + body.append("* [puzzles/{cat}/{points}](/puzzles/{cat}/{points}/)".format(cat=parts[2], points=points)) elif len(parts) == 4: body.append("# {} puzzle {}".format(parts[2], parts[3])) - with open("puzzles/{}/{}.moth".format(parts[2], parts[3])) as f: - p = puzzles.Puzzle(f) - body.append("* Author: `{}`".format(p.fields.get("author"))) - body.append("* Summary: `{}`".format(p.fields.get("summary"))) + fpath = os.path.join("puzzles", parts[2]) + cat = puzzles.Category(fpath, seed) + p = cat.puzzle(int(parts[3])) + body.append("* Author: `{}`".format(p['author'])) + body.append("* Summary: `{}`".format(p['summary'])) body.append('') body.append("## Body") body.append(p.body) - body.append("## Answers:") - for a in p.answers: + body.append("## Answers") + for a in p['answers']: body.append("* `{}`".format(a)) body.append("") else: @@ -121,7 +137,7 @@ you are a fool. self.serve_md() else: super().do_GET() - + def serve_md(self, text=None): fspathstr = self.translate_path(self.path) fspath = pathlib.Path(fspathstr) @@ -133,8 +149,9 @@ you are a fool. return None content = mdpage(text) - self.send_response(http.server.HTTPStatus.OK) - self.send_header("Content-type", "text/html; encoding=utf-8") + self.send_response(HTTPStatus.OK) + + self.send_header("Content-type", "text/html; charset=utf-8") self.send_header("Content-Length", len(content)) try: fs = fspath.stat() @@ -145,9 +162,9 @@ you are a fool. self.wfile.write(content.encode('utf-8')) -def run(address=('', 8080)): +def run(address=('localhost', 8080)): httpd = ThreadingServer(address, MothHandler) - print("=== Listening on http://{}:{}/".format(address[0] or "localhost", address[1])) + print("=== Listening on http://{}:{}/".format(address[0], address[1])) httpd.serve_forever() if __name__ == '__main__': diff --git a/mistune.py b/mistune.py index c0f976d..a81c4c1 100644 --- a/mistune.py +++ b/mistune.py @@ -335,7 +335,7 @@ class BlockLexer(object): rest = len(item) if i != length - 1 and rest: - _next = item[rest-1] == '\n' + _next = item[rest - 1] == '\n' if not loose: loose = _next diff --git a/puzzles.py b/puzzles.py index 5b3bd98..94741ce 100644 --- a/puzzles.py +++ b/puzzles.py @@ -56,7 +56,7 @@ class Puzzle: ANSWER_WORDS = [w.strip() for w in open(os.path.join(os.path.dirname(__file__), 'answer_words.txt'))] - def __init__(self, category_seed, path=None): + def __init__(self, category_seed, path=None, points=None): """A MOTH Puzzle :param category_seed: A byte string to use as a seed for random numbers for this puzzle. It is combined with the puzzle points. @@ -69,12 +69,16 @@ class Puzzle: (optional) A puzzle.py file. This is expected to have a callable called make that takes a single positional argument (this puzzle object). This callable can then do whatever it needs to with this object. + :param points: The point value of the puzzle. Mutually exclusive with path. If neither of the above are given, the point value for the puzzle will have to - be set manually. + be set at instantiation. """ super().__init__() + if (points is None and path is None) or (points is not None and path is not None): + raise ValueError("Either points or path must be set, but not both.") + self._dict = defaultdict(lambda: []) if os.path.isdir(path): self._puzzle_dir = path @@ -83,25 +87,24 @@ class Puzzle: self.message = bytes(random.choice(messageChars) for i in range(20)) self.body = '' - self._seed = hashlib.sha1(category_seed + bytes(self['points'])).digest() - self.rand = random.Random(self._seed) - - # Set our 'files' as a dict, since we want register them uniquely by name. - self['files'] = dict() - # A list of temporary files we've created that will need to be deleted. self._temp_files = [] + if path is not None: + if not os.path.isdir(path): + raise ValueError("No such directory: {}".format(path)) - # All internal variables must be initialized before the following runs - if not os.path.exists(path): - raise ValueError("No puzzle at path: {]".format(path)) - elif os.path.isdir(path): - # Expected format is path/.moth pathname = os.path.split(path)[-1] try: self.points = int(pathname) except ValueError: - pass + raise ValueError("Directory name must be a point value: {}".format(path)) + elif points is not None: + self.points = points + + self._seed = category_seed * self.points + self.rand = random.Random(self._seed) + + if path is not None: files = os.listdir(path) if 'puzzle.moth' in files: @@ -112,9 +115,10 @@ class Puzzle: loader = SourceFileLoader('puzzle_mod', os.path.join(path, 'puzzle.py')) puzzle_mod = loader.load_module() if hasattr(puzzle_mod, 'make'): + self.body = '# `puzzle.body` was not set by the `make` function' puzzle_mod.make(self) - else: - raise ValueError("Unacceptable file type for puzzle at {}".format(path)) + else: + self.body = '# `puzzle.py` does not define a `make` function' def cleanup(self): """Cleanup any outstanding temporary files.""" @@ -238,16 +242,13 @@ class Puzzle: def __getitem__(self, item): return self._dict[item.lower()] - def make_answer(self, word_count, sep=b' '): + def make_answer(self, word_count, sep=' '): """Generate and return a new answer. It's automatically added to the puzzle answer list. :param int word_count: The number of words to include in the answer. :param str|bytes sep: The word separator. - :returns: The answer bytes + :returns: The answer string """ - if type(sep) == str: - sep = sep.encode('ascii') - answer = sep.join(self.rand.sample(self.ANSWER_WORDS, word_count)) self['answer'] = answer @@ -271,8 +272,8 @@ class Puzzle: 'summary': self['summary'], } return obj - -if __name__ == '__main__': + +if __name__ == '__main__': parser = argparse.ArgumentParser(description='Build a puzzle category') parser.add_argument('puzzledir', nargs='+', help='Directory of puzzle source') args = parser.parse_args() @@ -291,3 +292,22 @@ if __name__ == '__main__': puzzle = puzzles[points] print(puzzle.secrets()) + +class Category: + def __init__(self, path, seed): + self.path = path + self.seed = seed + self.pointvals = [] + for fpath in glob.glob(os.path.join(path, "[0-9]*")): + pn = os.path.basename(fpath) + points = int(pn) + self.pointvals.append(points) + self.pointvals.sort() + + def puzzle(self, points): + path = os.path.join(self.path, str(points)) + return Puzzle(path, self.seed) + + def puzzles(self): + for points in self.pointvals: + yield self.puzzle(points) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3bf77b8 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,8 @@ +[flake8] +# flake8 is an automated code formatting pedant. +# Use it, please. +# +# python3 -m flake8 . +# +ignore = E501 +exclude = .git \ No newline at end of file