Merged in some changes.

This commit is contained in:
Paul Ferrell 2016-10-18 13:23:14 -06:00
commit f2c12aa3ea
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 glob
import http.server import http.server
import mistune import mistune
@ -7,6 +8,8 @@ import os
import pathlib import pathlib
import puzzles import puzzles
import socketserver import socketserver
import sys
import traceback
try: try:
from http.server import HTTPStatus from http.server import HTTPStatus
@ -15,6 +18,9 @@ except ImportError:
NOT_FOUND = 404 NOT_FOUND = 404
OK = 200 OK = 200
# XXX: This will eventually cause a problem. Do something more clever here.
seed = 1
def page(title, body): def page(title, body):
return """<!DOCTYPE html> return """<!DOCTYPE html>
@ -45,7 +51,20 @@ class ThreadingServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
pass 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): def do_GET(self):
if self.path == "/": if self.path == "/":
self.serve_front() self.serve_front()
@ -91,25 +110,22 @@ you are a fool.
elif len(parts) == 3: elif len(parts) == 3:
# List all point values in a category # List all point values in a category
body.append("# Puzzles in category `{}`".format(parts[2])) body.append("# Puzzles in category `{}`".format(parts[2]))
puzz = [] fpath = os.path.join("puzzles", parts[2])
for i in glob.glob(os.path.join("puzzles", parts[2], "*.moth")): cat = puzzles.Category(fpath, seed)
base = os.path.basename(i) for points in cat.pointvals:
root, _ = os.path.splitext(base) body.append("* [puzzles/{cat}/{points}](/puzzles/{cat}/{points}/)".format(cat=parts[2], points=points))
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))
elif len(parts) == 4: elif len(parts) == 4:
body.append("# {} puzzle {}".format(parts[2], parts[3])) body.append("# {} puzzle {}".format(parts[2], parts[3]))
with open("puzzles/{}/{}.moth".format(parts[2], parts[3])) as f: fpath = os.path.join("puzzles", parts[2])
p = puzzles.Puzzle(f) cat = puzzles.Category(fpath, seed)
body.append("* Author: `{}`".format(p.fields.get("author"))) p = cat.puzzle(int(parts[3]))
body.append("* Summary: `{}`".format(p.fields.get("summary"))) body.append("* Author: `{}`".format(p['author']))
body.append("* Summary: `{}`".format(p['summary']))
body.append('') body.append('')
body.append("## Body") body.append("## Body")
body.append(p.body) body.append(p.body)
body.append("## Answers:") body.append("## Answers")
for a in p.answers: for a in p['answers']:
body.append("* `{}`".format(a)) body.append("* `{}`".format(a))
body.append("") body.append("")
else: else:
@ -133,8 +149,9 @@ you are a fool.
return None return None
content = mdpage(text) content = mdpage(text)
self.send_response(http.server.HTTPStatus.OK) self.send_response(HTTPStatus.OK)
self.send_header("Content-type", "text/html; encoding=utf-8")
self.send_header("Content-type", "text/html; charset=utf-8")
self.send_header("Content-Length", len(content)) self.send_header("Content-Length", len(content))
try: try:
fs = fspath.stat() fs = fspath.stat()
@ -145,9 +162,9 @@ you are a fool.
self.wfile.write(content.encode('utf-8')) self.wfile.write(content.encode('utf-8'))
def run(address=('', 8080)): def run(address=('localhost', 8080)):
httpd = ThreadingServer(address, MothHandler) 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() httpd.serve_forever()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -335,7 +335,7 @@ class BlockLexer(object):
rest = len(item) rest = len(item)
if i != length - 1 and rest: if i != length - 1 and rest:
_next = item[rest-1] == '\n' _next = item[rest - 1] == '\n'
if not loose: if not loose:
loose = _next 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 = [w.strip() for w in open(os.path.join(os.path.dirname(__file__),
'answer_words.txt'))] 'answer_words.txt'))]
def __init__(self, category_seed, path=None): def __init__(self, category_seed, path=None, points=None):
"""A MOTH Puzzle """A MOTH Puzzle
:param category_seed: A byte string to use as a seed for random numbers for this 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. 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 (optional) A puzzle.py file. This is expected to have a callable called make
that takes a single positional argument (this puzzle object). that takes a single positional argument (this puzzle object).
This callable can then do whatever it needs to with this 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 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__() 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: []) self._dict = defaultdict(lambda: [])
if os.path.isdir(path): if os.path.isdir(path):
self._puzzle_dir = path self._puzzle_dir = path
@ -83,25 +87,24 @@ class Puzzle:
self.message = bytes(random.choice(messageChars) for i in range(20)) self.message = bytes(random.choice(messageChars) for i in range(20))
self.body = '' 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. # A list of temporary files we've created that will need to be deleted.
self._temp_files = [] 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] pathname = os.path.split(path)[-1]
try: try:
self.points = int(pathname) self.points = int(pathname)
except ValueError: 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) files = os.listdir(path)
if 'puzzle.moth' in files: if 'puzzle.moth' in files:
@ -112,9 +115,10 @@ class Puzzle:
loader = SourceFileLoader('puzzle_mod', os.path.join(path, 'puzzle.py')) loader = SourceFileLoader('puzzle_mod', os.path.join(path, 'puzzle.py'))
puzzle_mod = loader.load_module() puzzle_mod = loader.load_module()
if hasattr(puzzle_mod, 'make'): if hasattr(puzzle_mod, 'make'):
self.body = '# `puzzle.body` was not set by the `make` function'
puzzle_mod.make(self) puzzle_mod.make(self)
else: else:
raise ValueError("Unacceptable file type for puzzle at {}".format(path)) self.body = '# `puzzle.py` does not define a `make` function'
def cleanup(self): def cleanup(self):
"""Cleanup any outstanding temporary files.""" """Cleanup any outstanding temporary files."""
@ -238,16 +242,13 @@ class Puzzle:
def __getitem__(self, item): def __getitem__(self, item):
return self._dict[item.lower()] 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. """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 int word_count: The number of words to include in the answer.
:param str|bytes sep: The word separator. :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)) answer = sep.join(self.rand.sample(self.ANSWER_WORDS, word_count))
self['answer'] = answer self['answer'] = answer
@ -291,3 +292,22 @@ if __name__ == '__main__':
puzzle = puzzles[points] puzzle = puzzles[points]
print(puzzle.secrets()) 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