Merge pull request #191 from lykoss/proxy

Introduce proxies as a means of breaking circular import chains
This commit is contained in:
Emanuel Barry 2015-11-15 18:08:06 -05:00
commit 837cf49c44
5 changed files with 149 additions and 38 deletions

View File

@ -5,6 +5,32 @@ import time
import botconfig import botconfig
import src.settings as var import src.settings as var
# Segue to logger, since src.gamemodes requires it
# TODO: throw this into a logger.py perhaps so we aren't breaking up imports with non-import stuff
def logger(file, write=True, display=True):
if file is not None:
open(file, "a").close() # create the file if it doesn't exist
def log(*output, write=write, display=display):
output = " ".join([str(x) for x in output]).replace("\u0002", "").replace("\\x02", "") # remove bold
if botconfig.DEBUG_MODE:
write = True
if botconfig.DEBUG_MODE or botconfig.VERBOSE_MODE:
display = True
timestamp = get_timestamp()
if display:
print(timestamp + output, file=utf8stdout)
if write and file is not None:
with open(file, "a", errors="replace") as f:
f.seek(0, 2)
f.write(timestamp + output + "\n")
return log
stream_handler = logger(None)
debuglog = logger("debug.log", write=False, display=False)
errlog = logger("errors.log")
plog = logger(None) # use this instead of print so that logs have timestamps
# Import the user-defined game modes # Import the user-defined game modes
# These are not required, so failing to import it doesn't matter # These are not required, so failing to import it doesn't matter
# The file then imports our game modes # The file then imports our game modes
@ -86,28 +112,6 @@ def get_timestamp(use_utc=None, ts_format=None):
offset += str(time.timezone // 36).zfill(4) offset += str(time.timezone // 36).zfill(4)
return tmf.format(tzname=tz, tzoffset=offset).strip().upper() + " " return tmf.format(tzname=tz, tzoffset=offset).strip().upper() + " "
def logger(file, write=True, display=True):
if file is not None:
open(file, "a").close() # create the file if it doesn't exist
def log(*output, write=write, display=display):
output = " ".join([str(x) for x in output]).replace("\u0002", "").replace("\\x02", "") # remove bold
if botconfig.DEBUG_MODE:
write = True
if botconfig.DEBUG_MODE or botconfig.VERBOSE_MODE:
display = True
timestamp = get_timestamp()
if display:
print(timestamp + output, file=utf8stdout)
if write and file is not None:
with open(file, "a", errors="replace") as f:
f.seek(0, 2)
f.write(timestamp + output + "\n")
return log
stream_handler = logger(None)
debuglog = logger("debug.log", write=False, display=False)
def stream(output, level="normal"): def stream(output, level="normal"):
if botconfig.VERBOSE_MODE or botconfig.DEBUG_MODE: if botconfig.VERBOSE_MODE or botconfig.DEBUG_MODE:
stream_handler(output) stream_handler(output)

View File

@ -7,8 +7,7 @@ import botconfig
import src.settings as var import src.settings as var
from src.utilities import * from src.utilities import *
from src.messages import messages from src.messages import messages
# utilities imported per Vgr's temporary fix to chk_nightdone(cli) in Sleepy from src import events
from src import events, utilities
def game_mode(name, minp, maxp, likelihood = 0): def game_mode(name, minp, maxp, likelihood = 0):
def decor(c): def decor(c):
@ -852,7 +851,7 @@ class SleepyMode(GameMode):
if "correct" in self.on_path: if "correct" in self.on_path:
pm(cli, self.having_nightmare, messages["sleepy_nightmare_wake"]) pm(cli, self.having_nightmare, messages["sleepy_nightmare_wake"])
self.having_nightmare = None self.having_nightmare = None
utilities.chk_nightdone(cli) chk_nightdone(cli)
elif "fake1" in self.on_path: elif "fake1" in self.on_path:
pm(cli, self.having_nightmare, messages["sleepy_nightmare_fake_1"]) pm(cli, self.having_nightmare, messages["sleepy_nightmare_fake_1"])
self.step = 0 self.step = 0

101
src/proxy.py Normal file
View File

@ -0,0 +1,101 @@
import inspect
from src.decorators import handle_error
""" This module introduces two decorators - @proxy.stub and @proxy.impl
@proxy.stub is used to decorate a stub method that should be filled in
with an implementation in some other module. Calling the stub method
calls the implementation method in the other module instead of the stub
method itself (or raises a NotImplementedError if no such
implementation exists)
@proxy.impl is used to define a previously-declared stub with an actual
implementation to call -- the signature for the implementation must
exactly match the signature for the stub (enforced for Python 3.3+).
Attempting to implement a non-existent stub is an error, as is trying
to re-implement a stub that is already implemented.
Example:
(foo.py)
@proxy.stub
def my_method(foo, bar=10):
pass
(bar.py)
@proxy.impl
def my_method(foo, bar=10):
return foo * bar
"""
IMPLS = {}
SIGS = {}
def stub(f):
def inner(*args, **kwargs):
if f.__name__ not in IMPLS:
raise NotImplementedError(("This proxy stub has not yet been "
"implemented in another module"))
return IMPLS[f.__name__](*args, **kwargs)
if hasattr(inspect, "signature"):
# Python 3.3+
if f.__name__ in SIGS:
_sigmatch(f)
SIGS[f.__name__] = inspect.signature(f)
else:
# Python 3.2; not allowed to have nice things
SIGS[f.__name__] = None
return inner
def impl(f):
if f.__name__ not in SIGS:
raise NameError(("Attempting to implement a proxy stub {0} that does "
"not exist").format(f.__name__))
if f.__name__ in IMPLS:
raise SyntaxError(("Attempting to implement a proxy stub {0} that "
"already has an implementation").format(f.__name__))
if hasattr(inspect, "signature"):
# Python 3.3+
_sigmatch(f)
# Always wrap proxy implementations in an error handler
IMPLS[f.__name__] = handle_error(f)
# allows this method to be called directly in our module rather
# than forcing use of the stub's module
return handle_error(f)
def _sigmatch(f):
rs = inspect.signature(f)
ts = SIGS[f.__name__]
if len(rs.parameters) != len(ts.parameters):
raise TypeError(
("Arity does not match existing stub, "
"expected {0} parameters but got {1}.").format(
len(ts.parameters), len(rs.parameters)))
opl = list(rs.parameters.values())
tpl = list(ts.parameters.values())
for i in range(len(rs.parameters)):
op = opl[i]
tp = tpl[i]
if op.name != tp.name:
raise TypeError(
("Parameter name does not match existing stub, "
"expected {0} but got {1}.").format(tp.name, op.name))
if op.default != tp.default:
raise TypeError(
("Default value of parameter does not match existing stub "
"for parameter {0}, expected {1} but got {2}.").format(
op.name,
("no default" if tp.default is inspect.Parameter.empty
else repr(tp.default)),
("no default" if op.default is inspect.Parameter.empty
else repr(op.default))))
if op.kind != tp.kind:
raise TypeError(
("Parameter type does not match existing stub for "
"parameter {0}, expected {1} but got {2}.").format(
op.name, str(tp.kind), str(op.kind)))
# vim: set expandtab:sw=4:ts=4:

View File

@ -1,7 +1,9 @@
import src.settings as var
import botconfig
import re import re
import botconfig
import src.settings as var
from src import proxy, debuglog
# Some miscellaneous helper functions # Some miscellaneous helper functions
def mass_mode(cli, md_param, md_plain): def mass_mode(cli, md_param, md_plain):
@ -156,4 +158,16 @@ def relay_wolfchat_command(cli, nick, message, roles, is_wolf_command=False, is_
mass_privmsg(cli, wcwolves, message) mass_privmsg(cli, wcwolves, message)
mass_privmsg(cli, var.SPECTATING_WOLFCHAT, "[wolfchat] " + message) mass_privmsg(cli, var.SPECTATING_WOLFCHAT, "[wolfchat] " + message)
@proxy.stub
def chk_nightdone(cli):
pass
@proxy.stub
def chk_decision(cli, force=""):
pass
@proxy.stub
def chk_win(cli, end_game=True, winner=None):
pass
# vim: set expandtab:sw=4:ts=4: # vim: set expandtab:sw=4:ts=4:

View File

@ -44,19 +44,12 @@ from oyoyo.parse import parse_nick
import botconfig import botconfig
import src.settings as var import src.settings as var
from src.utilities import * from src.utilities import *
from src import decorators, events, logger, utilities, debuglog from src import decorators, events, logger, proxy, debuglog, errlog, plog
# make debuglog accessible anywhere
utilities.debuglog = debuglog
from src.messages import messages from src.messages import messages
# done this way so that events is accessible in !eval (useful for debugging) # done this way so that events is accessible in !eval (useful for debugging)
Event = events.Event Event = events.Event
errlog = logger("errors.log")
plog = logger(None) #use this instead of print so that logs have timestamps
is_admin = var.is_admin is_admin = var.is_admin
is_owner = var.is_owner is_owner = var.is_owner
@ -2073,7 +2066,7 @@ def fday(cli, nick, chan, rest):
transition_day(cli) transition_day(cli)
# Specify force = "nick" to force nick to be lynched # Specify force = "nick" to force nick to be lynched
@handle_error @proxy.impl
def chk_decision(cli, force = ""): def chk_decision(cli, force = ""):
with var.GRAVEYARD_LOCK: with var.GRAVEYARD_LOCK:
if var.PHASE != "day": if var.PHASE != "day":
@ -2595,6 +2588,7 @@ def stop_game(cli, winner = "", abort = False, additional_winners = None):
return True return True
@proxy.impl
def chk_win(cli, end_game=True, winner=None): def chk_win(cli, end_game=True, winner=None):
""" Returns True if someone won """ """ Returns True if someone won """
chan = botconfig.CHANNEL chan = botconfig.CHANNEL
@ -4603,6 +4597,7 @@ def transition_day(cli, gameid=0):
begin_day(cli) begin_day(cli)
@proxy.impl
def chk_nightdone(cli): def chk_nightdone(cli):
if var.PHASE != "night": if var.PHASE != "night":
return return
@ -4702,8 +4697,6 @@ def chk_nightdone(cli):
if var.PHASE == "night": # Double check if var.PHASE == "night": # Double check
transition_day(cli) transition_day(cli)
utilities.chk_nightdone = chk_nightdone # for some events to access
@cmd("nolynch", "nl", "novote", "nv", "abstain", "abs", playing=True, phases=("day",)) @cmd("nolynch", "nl", "novote", "nv", "abstain", "abs", playing=True, phases=("day",))
def no_lynch(cli, nick, chan, rest): def no_lynch(cli, nick, chan, rest):
"""Allows you to abstain from voting for the day.""" """Allows you to abstain from voting for the day."""