From 6f83a909df4d3b7947bea1f304ba16c885b6ed72 Mon Sep 17 00:00:00 2001 From: skizzerz Date: Wed, 11 Nov 2015 12:57:11 -0600 Subject: [PATCH 1/3] 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.""" From a26f912f6ee28976b71df110f34acb5db0b8b2e0 Mon Sep 17 00:00:00 2001 From: skizzerz Date: Sun, 15 Nov 2015 13:51:46 -0600 Subject: [PATCH 2/3] Wrap the impl side of implementations in handle_error unconditionally as well --- src/proxy.py | 2 +- src/wolfgame.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/proxy.py b/src/proxy.py index 4a43004..2e08803 100644 --- a/src/proxy.py +++ b/src/proxy.py @@ -59,7 +59,7 @@ def impl(f): 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 + return handle_error(f) def _sigmatch(f): rs = inspect.signature(f) diff --git a/src/wolfgame.py b/src/wolfgame.py index 9f6228c..db6938a 100644 --- a/src/wolfgame.py +++ b/src/wolfgame.py @@ -2066,7 +2066,6 @@ def fday(cli, nick, chan, rest): transition_day(cli) # Specify force = "nick" to force nick to be lynched -@handle_error @proxy.impl def chk_decision(cli, force = ""): with var.GRAVEYARD_LOCK: From 8a36dd844251b7e6293f781c0750943422b70b75 Mon Sep 17 00:00:00 2001 From: skizzerz Date: Sun, 15 Nov 2015 17:06:46 -0600 Subject: [PATCH 3/3] Fix docstring --- src/proxy.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/proxy.py b/src/proxy.py index 2e08803..eb12283 100644 --- a/src/proxy.py +++ b/src/proxy.py @@ -3,21 +3,26 @@ 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):