Merged in some changes.

This commit is contained in:
Paul Ferrell 2016-10-18 13:23:14 -06:00
commit f27c283e0e
5 changed files with 94 additions and 46 deletions

3
CREDITS.md Normal file
View File

@ -0,0 +1,3 @@
Neale Pickett
Patrick Avery
Shannon Steinfadt

View File

@ -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 """<!DOCTYPE html>
@ -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__':

View File

@ -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

View File

@ -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/<points_int>.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)

8
setup.cfg Normal file
View File

@ -0,0 +1,8 @@
[flake8]
# flake8 is an automated code formatting pedant.
# Use it, please.
#
# python3 -m flake8 .
#
ignore = E501
exclude = .git