From 6f83a909df4d3b7947bea1f304ba16c885b6ed72 Mon Sep 17 00:00:00 2001 From: skizzerz Date: Wed, 11 Nov 2015 12:57:11 -0600 Subject: [PATCH] Introduce proxies as a means of breaking circular import chains --- src/__init__.py | 48 +++++++++++++----------- src/gamemodes.py | 5 +-- src/proxy.py | 96 ++++++++++++++++++++++++++++++++++++++++++++++++ src/utilities.py | 18 ++++++++- src/wolfgame.py | 14 ++----- 5 files changed, 144 insertions(+), 37 deletions(-) create mode 100644 src/proxy.py diff --git a/src/__init__.py b/src/__init__.py index 2682f23..ac5b109 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -5,6 +5,32 @@ import time import botconfig 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 # These are not required, so failing to import it doesn't matter # 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) 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"): if botconfig.VERBOSE_MODE or botconfig.DEBUG_MODE: stream_handler(output) diff --git a/src/gamemodes.py b/src/gamemodes.py index 2ad54a4..5110a9b 100644 --- a/src/gamemodes.py +++ b/src/gamemodes.py @@ -7,8 +7,7 @@ import botconfig import src.settings as var from src.utilities import * from src.messages import messages -# utilities imported per Vgr's temporary fix to chk_nightdone(cli) in Sleepy -from src import events, utilities +from src import events def game_mode(name, minp, maxp, likelihood = 0): def decor(c): @@ -852,7 +851,7 @@ class SleepyMode(GameMode): if "correct" in self.on_path: pm(cli, self.having_nightmare, messages["sleepy_nightmare_wake"]) self.having_nightmare = None - utilities.chk_nightdone(cli) + chk_nightdone(cli) elif "fake1" in self.on_path: pm(cli, self.having_nightmare, messages["sleepy_nightmare_fake_1"]) self.step = 0 diff --git a/src/proxy.py b/src/proxy.py new file mode 100644 index 0000000..4a43004 --- /dev/null +++ b/src/proxy.py @@ -0,0 +1,96 @@ +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 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: diff --git a/src/utilities.py b/src/utilities.py index 4cb75cf..339c082 100644 --- a/src/utilities.py +++ b/src/utilities.py @@ -1,7 +1,9 @@ -import src.settings as var -import botconfig import re +import botconfig +import src.settings as var +from src import proxy, debuglog + # Some miscellaneous helper functions 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, 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: diff --git a/src/wolfgame.py b/src/wolfgame.py index a868b5d..9f6228c 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -44,19 +44,12 @@ from oyoyo.parse import parse_nick import botconfig import src.settings as var from src.utilities import * -from src import decorators, events, logger, utilities, debuglog - -# make debuglog accessible anywhere -utilities.debuglog = debuglog - +from src import decorators, events, logger, proxy, debuglog, errlog, plog from src.messages import messages # done this way so that events is accessible in !eval (useful for debugging) 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_owner = var.is_owner @@ -2074,6 +2067,7 @@ def fday(cli, nick, chan, rest): # Specify force = "nick" to force nick to be lynched @handle_error +@proxy.impl def chk_decision(cli, force = ""): with var.GRAVEYARD_LOCK: if var.PHASE != "day": @@ -2595,6 +2589,7 @@ def stop_game(cli, winner = "", abort = False, additional_winners = None): return True +@proxy.impl def chk_win(cli, end_game=True, winner=None): """ Returns True if someone won """ chan = botconfig.CHANNEL @@ -4604,6 +4599,7 @@ def transition_day(cli, gameid=0): begin_day(cli) +@proxy.impl def chk_nightdone(cli): if var.PHASE != "night": return @@ -4703,8 +4699,6 @@ def chk_nightdone(cli): if var.PHASE == "night": # Double check transition_day(cli) -utilities.chk_nightdone = chk_nightdone # for some events to access - @cmd("nolynch", "nl", "novote", "nv", "abstain", "abs", playing=True, phases=("day",)) def no_lynch(cli, nick, chan, rest): """Allows you to abstain from voting for the day."""