From 4678234e9a9777d795eff4714703c644c84e316c Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 22 Apr 2012 12:23:42 +0200 Subject: [PATCH] Multiple fixes to ev and utils: Made utils.variable_from_module more generic (it can now load pretty much any form of module it's given and also supports searching and returning multiple variables). Removed the variable-load functionality from utils.load_module; this is now purely a loader - use variable_from_module instead. I found out that one couldn't import from src.commands.default due to the __init__ file being restrictive for the sake of the ev API. Removed that and instead imported the default commands into ev.py with the help of utils.variable_from_module instead. Some more fixes in ev followed on this. --- ev.py | 96 ++++++++++++------- src/commands/cmdhandler.py | 2 +- src/commands/default/__init__.py | 25 +---- src/commands/default/cmdset_default.py | 5 - src/commands/default/cmdset_ooc.py | 16 ++-- src/commands/default/general.py | 2 +- src/objects/manager.py | 2 +- src/objects/models.py | 4 +- src/players/models.py | 2 +- src/utils/dummyrunner.py | 118 +++++++++++------------ src/utils/utils.py | 127 ++++++++++++++----------- 11 files changed, 211 insertions(+), 188 deletions(-) diff --git a/ev.py b/ev.py index de9542f35..8b3bd3866 100644 --- a/ev.py +++ b/ev.py @@ -2,21 +2,18 @@ Central API for the Evennia MUD/MUX/MU* creation system. -This basically a set of shortcuts to the main modules in src/. Import this from your code or -explore it interactively from a python shell. +This is basically a set of shortcuts for accessing things in src/ with less boiler plate. +Import this from your code or explore it interactively from a python shell. Notes: 1) You should import things explicitly from the root of this module - you can not use - dot-notation to import deeper. Hence, to access a default command, you can do the - following: - + dot-notation to import deeper. Hence, to access a default command, you can do import ev ev.default_cmds.CmdLook - or + or from ev import default_cmds default_cmds.CmdLook - But trying to import CmdLook directly with "from ev.default_cmds import CmdLook" will not work since default_cmds is a property on the "ev" module, not a module of its own. @@ -38,34 +35,48 @@ Notes: some more error checking. 5) "settings" links to Evennia's game/settings file. "settings_full" shows all of django's available - settings. Note that you cannot change settings from here in a meaningful way, you need to update - game/settings.py and restart the server. + settings. Note that this is for viewing only - you cannot *change* settings from here in a meaningful + way but have to update game/settings.py and restart the server. - 6) The API accesses all relevant and most-neeeded functions/classes from src/, but might not + 6) The API accesses all relevant and most-neeeded functions/classes from src/ but might not always include all helper-functions referenced from each such entity. To get to those, access the modules in src/ directly. You can always do this anyway, if you do not want to go through this API. - """ + import sys, os +###################################################################### +# set Evennia version in __version__ property +###################################################################### + +try: + f = open(os.path.dirname(os.path.abspath(__file__)) + os.sep + "VERSION", 'r') + __version__ = "Evennia %s-r%s" % (f.read().strip(), os.popen("hg id -i").read().strip()) + f.close() + del f +except IOError: + __version__ = "Evennia (unknown version)" + ###################################################################### # Stop erroneous direct run (would give a traceback since django is # not yet initialized) ###################################################################### if __name__ == "__main__": - info = __doc__ + \ + print \ """ - | This module gives access to Evennia's programming API. It should - | not be run on its own, but be imported and accessed as described - | above. - | - | To start the Evennia server, see game/manage.py and game/evennia.py. - | More help can be found at http://www.evennia.com. -""" - print info + Evennia MU* creation system (%s) + + This module gives access to Evennia's API (Application Programming + Interface). It should *not* be run on its own, but be imported and + accessed from your code or explored interactively from a Python + shell. + + For help configuring and starting the Evennia server, see the + INSTALL file. More help can be found at http://www.evennia.com. +""" % __version__ sys.exit() ###################################################################### @@ -79,18 +90,6 @@ from game import settings setup_environ(settings) del setup_environ from django.conf import settings as settings_full - -###################################################################### -# set Evennia version in __version__ property -###################################################################### - -try: - f = open(os.path.dirname(os.path.abspath(__file__)) + os.sep + "VERSION", 'r') - __version__ = "Evennia %s-r%s" % (f.read().strip(), os.popen("hg id -i").read().strip()) - f.close() - del f -except IOError: - __version__ = "Evennia (unknown version)" del sys, os ###################################################################### @@ -110,7 +109,7 @@ from src.players.models import PlayerDB, PlayerAttribute, PlayerNick # commands from src.commands.command import Command from src.commands.cmdset import CmdSet -from src.commands import default as default_cmds +# (default_cmds is created below) # locks from src.locks import lockfuncs @@ -175,6 +174,37 @@ class DBmanagers(object): managers = DBmanagers() del DBmanagers +class DefaultCmds(object): + """ + This container holds direct shortcuts to all default commands in + Evennia. + """ + + from src.commands.default.cmdset_default import DefaultCmdSet + from src.commands.default.cmdset_ooc import OOCCmdSet + from src.commands.default.cmdset_unloggedin import UnloggedinCmdSet + from src.commands.default.muxcommand import MuxCommand + + def __init__(self): + "populate the object with commands" + + def add_cmds(module): + "helper method for populating this object with cmds" + cmdlist = utils.variable_from_module(module, module.__all__) + self.__dict__.update(dict([(c.__name__, c) for c in cmdlist])) + + from src.commands.default import admin, batchprocess, building, comms, general, help, system, unloggedin + add_cmds(building) + add_cmds(batchprocess) + add_cmds(building) + add_cmds(comms) + add_cmds(general) + add_cmds(help) + add_cmds(system) + add_cmds(unloggedin) +default_cmds = DefaultCmds() +del DefaultCmds + class SystemCmds(object): """ Creating commands with keys set to these constants will make diff --git a/src/commands/cmdhandler.py b/src/commands/cmdhandler.py index a944a6c55..7d235c8b1 100644 --- a/src/commands/cmdhandler.py +++ b/src/commands/cmdhandler.py @@ -47,7 +47,7 @@ __all__ = ("cmdhandler",) # This decides which command parser is to be used. # You have to restart the server for changes to take effect. -_COMMAND_PARSER = utils.mod_import(*settings.COMMAND_PARSER.rsplit('.', 1)) +_COMMAND_PARSER = utils.variable_from_module(*settings.COMMAND_PARSER.rsplit('.', 1)) # System command names - import these variables rather than trying to # remember the actual string constants. If not defined, Evennia diff --git a/src/commands/default/__init__.py b/src/commands/default/__init__.py index de9b0251e..b75fd526e 100644 --- a/src/commands/default/__init__.py +++ b/src/commands/default/__init__.py @@ -1,27 +1,4 @@ """ -Groups all default commands for access from the API. - -To use via ev API, import this module with - from ev import default_cmds, -you can then access the command classes as members of default_cmds. +This package contains all default commands of Evennia, grouped after category. """ -from src.commands.default.cmdset_default import DefaultCmdSet -from src.commands.default.cmdset_ooc import OOCCmdSet -from src.commands.default.cmdset_unloggedin import UnloggedinCmdSet - -from src.commands.default.muxcommand import MuxCommand - -from src.commands.default.admin import * -from src.commands.default.batchprocess import * -from src.commands.default.building import * -from src.commands.default.comms import * -from src.commands.default.general import * -from src.commands.default.help import * -from src.commands.default import syscommands -from src.commands.default.system import * -from src.commands.default.unloggedin import * - -del cmdset_default, cmdset_ooc, cmdset_unloggedin, muxcommand -del admin, batchprocess, building, comms, general, -del help, system, unloggedin diff --git a/src/commands/default/cmdset_default.py b/src/commands/default/cmdset_default.py index 0290c4c55..1c56937d0 100644 --- a/src/commands/default/cmdset_default.py +++ b/src/commands/default/cmdset_default.py @@ -35,9 +35,6 @@ class DefaultCmdSet(CmdSet): self.add(help.CmdSetHelp()) # System commands - self.add(system.CmdReload()) - self.add(system.CmdReset()) - self.add(system.CmdShutdown()) self.add(system.CmdPy()) self.add(system.CmdScripts()) self.add(system.CmdObjects()) @@ -51,9 +48,7 @@ class DefaultCmdSet(CmdSet): self.add(admin.CmdBoot()) self.add(admin.CmdBan()) self.add(admin.CmdUnban()) - self.add(admin.CmdDelPlayer()) self.add(admin.CmdEmit()) - self.add(admin.CmdNewPassword()) self.add(admin.CmdPerm()) self.add(admin.CmdWall()) diff --git a/src/commands/default/cmdset_ooc.py b/src/commands/default/cmdset_ooc.py index cb5331351..f28eb04db 100644 --- a/src/commands/default/cmdset_ooc.py +++ b/src/commands/default/cmdset_ooc.py @@ -6,20 +6,20 @@ a Player object as caller rather than a Character. """ from src.commands.cmdset import CmdSet -from src.commands.default import help, comms, general, admin +from src.commands.default import help, comms, general, admin, system class OOCCmdSet(CmdSet): """ Implements the player command set. """ - + key = "DefaultOOC" priority = -5 def at_cmdset_creation(self): "Populates the cmdset" - # general commands + # General commands self.add(general.CmdOOCLook()) self.add(general.CmdIC()) self.add(general.CmdOOC()) @@ -27,11 +27,15 @@ class OOCCmdSet(CmdSet): self.add(general.CmdQuit()) self.add(general.CmdPassword()) - # help command + # Help command self.add(help.CmdHelp()) - # admin commands - self.add(admin.CmdBoot()) + # system commands + self.add(system.CmdReload()) + self.add(system.CmdReset()) + self.add(system.CmdShutdown()) + + # Admin commands self.add(admin.CmdDelPlayer()) self.add(admin.CmdNewPassword()) diff --git a/src/commands/default/general.py b/src/commands/default/general.py index 8ff336d28..83fc6925e 100644 --- a/src/commands/default/general.py +++ b/src/commands/default/general.py @@ -15,7 +15,7 @@ __all__ = ("CmdHome", "CmdLook", "CmdPassword", "CmdNick", "CmdSay", "CmdPose", "CmdEncoding", "CmdAccess", "CmdOOCLook", "CmdIC", "CmdOOC") -AT_SEARCH_RESULT = utils.mod_import(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) +AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) BASE_PLAYER_TYPECLASS = settings.BASE_PLAYER_TYPECLASS class CmdHome(MuxCommand): diff --git a/src/objects/manager.py b/src/objects/manager.py index 4dbb179a7..742c86ee5 100644 --- a/src/objects/manager.py +++ b/src/objects/manager.py @@ -14,7 +14,7 @@ __all__ = ("ObjectManager",) # Try to use a custom way to parse id-tagged multimatches. -_AT_MULTIMATCH_INPUT = utils.mod_import(*settings.SEARCH_AT_MULTIMATCH_INPUT.rsplit('.', 1)) +_AT_MULTIMATCH_INPUT = utils.variable_from_module(*settings.SEARCH_AT_MULTIMATCH_INPUT.rsplit('.', 1)) class ObjectManager(TypedObjectManager): """ diff --git a/src/objects/models.py b/src/objects/models.py index cbe9f0dd7..18e723734 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -28,12 +28,12 @@ from src.commands.cmdsethandler import CmdSetHandler from src.commands import cmdhandler from src.scripts.scripthandler import ScriptHandler from src.utils import logger -from src.utils.utils import make_iter, to_unicode, mod_import +from src.utils.utils import make_iter, to_unicode, variable_from_module #__all__ = ("ObjAttribute", "Alias", "ObjectNick", "ObjectDB") -_AT_SEARCH_RESULT = mod_import(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) +_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) _GA = object.__getattribute__ _SA = object.__setattr__ diff --git a/src/players/models.py b/src/players/models.py index 44bef9577..da90e7471 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -55,7 +55,7 @@ from src.commands import cmdhandler __all__ = ("PlayerAttribute", "PlayerNick", "PlayerDB") -_AT_SEARCH_RESULT = utils.mod_import(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) +_AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) #------------------------------------------------------------ # diff --git a/src/utils/dummyrunner.py b/src/utils/dummyrunner.py index 6a92245a4..2f0a4072a 100644 --- a/src/utils/dummyrunner.py +++ b/src/utils/dummyrunner.py @@ -6,13 +6,13 @@ an Evennia game. It will launch any number of fake clients. These clients will log into the server and start doing random operations. Customizing and weighing these operations differently depends on which type of game is tested. The module contains a testing module -for plain Evennia. +for plain Evennia. -Please note that you shouldn't run this on a production server! -Launch the program without any arguments or options to see a -full step-by-step setup help. +Please note that you shouldn't run this on a production server! +Launch the program without any arguments or options to see a +full step-by-step setup help. -Basically (for testing default Evennia): +Basically (for testing default Evennia): - Use an empty/testing database. - set PERMISSION_PLAYER_DEFAULT = "Builders" @@ -22,7 +22,7 @@ Basically (for testing default Evennia): If you want to customize the runner's client actions (because you changed the cmdset or needs to better match your use cases or add more actions), you can -change which actions by adding a path to +change which actions by adding a path to DUMMYRUNNER_ACTIONS_MODULE = @@ -31,13 +31,13 @@ for instructions on how to define this module. """ -import os, sys, time, random +import os, sys, time, random from optparse import OptionParser from twisted.conch import telnet from twisted.internet import reactor, protocol # from twisted.application import internet, service # from twisted.web import client -from twisted.internet.task import LoopingCall +from twisted.internet.task import LoopingCall # Tack on the root evennia directory to the python path and initialize django settings sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) @@ -49,7 +49,7 @@ from django.conf import settings from src.utils import utils HELPTEXT = """ -DO NOT RUN THIS ON A PRODUCTION SERVER! USE A CLEAN/TESTING DATABASE! +DO NOT RUN THIS ON A PRODUCTION SERVER! USE A CLEAN/TESTING DATABASE! This stand-alone program launches dummy telnet clients against a running Evennia server. The idea is to mimic real players logging in @@ -61,28 +61,28 @@ running clients will create new objects and rooms all over the place as part of their running, so using a clean/testing database is strongly recommended. -Setup: +Setup: 1) setup a fresh/clean database (if using sqlite, just safe-copy away your real evennia.db3 file and create a new one with manage.py) - 2) in game/settings.py, add - + 2) in game/settings.py, add + PERMISSION_PLAYER_DEFAULT="Builders" - 3a) Start Evennia like normal. + 3a) Start Evennia like normal. 3b) If you want profiling, start Evennia like this instead: - python runner.py -S start + python runner.py -S start this will start Evennia under cProfiler with output server.prof. - 4) run this dummy runner: + 4) run this dummy runner: python dummyclients.py [timestep] [port] Default is to connect one client to port 4000, using a 5 second timestep. Increase the number of clients and shorten the timestep (minimum is 1s) to further stress the game. - + You can stop the dummy runner with Ctrl-C. 5) Log on and determine if game remains responsive despite the @@ -98,12 +98,12 @@ DEFAULT_NCLIENTS = 1 # time between each 'tick', in seconds, if not set on command # line. All launched clients will be called upon to possibly do an # action with this frequency. -DEFAULT_TIMESTEP = 5 +DEFAULT_TIMESTEP = 5 # Port to use, if not specified on command line DEFAULT_PORT = settings.TELNET_PORTS[0] # chance of an action happening, per timestep. This helps to # spread out usage randomly, like it would be in reality. -CHANCE_OF_ACTION = 0.1 +CHANCE_OF_ACTION = 0.1 #------------------------------------------------------------ @@ -111,37 +111,37 @@ CHANCE_OF_ACTION = 0.1 #------------------------------------------------------------ def idcounter(): - "generates subsequent id numbers" - idcount = 0 + "generates subsequent id numbers" + idcount = 0 while True: - idcount += 1 + idcount += 1 yield idcount -OID = idcounter() +OID = idcounter() CID = idcounter() -def makeiter(obj): +def makeiter(obj): "makes everything iterable" if not hasattr(obj, '__iter__'): return [obj] - return obj - + return obj + #------------------------------------------------------------ # Client classes #------------------------------------------------------------ class DummyClient(telnet.StatefulTelnetProtocol): """ - Handles connection to a running Evennia server, - mimicking a real player by sending commands on - a timer. + Handles connection to a running Evennia server, + mimicking a real player by sending commands on + a timer. """ - - def connectionMade(self): + + def connectionMade(self): # public properties - self.cid = CID.next() + self.cid = CID.next() self.istep = 0 - self.exits = [] # exit names created + self.exits = [] # exit names created self.objs = [] # obj names created self._actions = self.factory.actions @@ -150,16 +150,16 @@ class DummyClient(telnet.StatefulTelnetProtocol): #print " ** client %i connected." % self.cid reactor.addSystemEventTrigger('before', 'shutdown', self.logout) - + # start client tick d = LoopingCall(self.step) d.start(self.factory.timestep, now=True).addErrback(self.error) - + def dataReceived(self, data): "Echo incoming data to stdout" if self._echo_all: print data - + def connectionLost(self, reason): "loosing the connection" #print " ** client %i lost connection." % self.cid @@ -175,25 +175,25 @@ class DummyClient(telnet.StatefulTelnetProtocol): def logout(self): "Causes the client to log out of the server. Triggered by ctrl-c signal." cmd, report = self._actions[1](self) - print "client %i %s (%s actions)" % (self.cid, report, self.istep) + print "client %i %s (%s actions)" % (self.cid, report, self.istep) self.sendLine(cmd) def step(self): """ Perform a step. This is called repeatedly by the runner and causes the client to issue commands to the server. - This holds all "intelligence" of the dummy client. + This holds all "intelligence" of the dummy client. """ - if random.random() > CHANCE_OF_ACTION: - return - if self.istep == 0: + if random.random() > CHANCE_OF_ACTION: + return + if self.istep == 0: cfunc = self._actions[0] else: # random selection using cumulative probabilities rand = random.random() cfunc = [func for cprob, func in self._actions[2] if cprob >= rand][0] # launch the action (don't hide tracebacks) cmd, report = cfunc(self) - # handle the result + # handle the result cmd = "\n".join(makeiter(cmd)) if self.istep == 0 or self._echo_brief or self._echo_all: print "client %i %s" % (self.cid, report) @@ -205,74 +205,74 @@ class DummyFactory(protocol.ClientFactory): def __init__(self, actions, timestep, verbose): "Setup the factory base (shared by all clients)" - self.actions = actions + self.actions = actions self.timestep = timestep self.verbose = verbose #------------------------------------------------------------ -# Access method: +# Access method: # Starts clients and connects them to a running server. #------------------------------------------------------------ def start_all_dummy_clients(actions, nclients=1, timestep=5, telnet_port=4000, verbose=0): # validating and preparing the action tuple - + # make sure the probabilities add up to 1 pratio = 1.0 / sum(tup[0] for tup in actions[2:]) flogin, flogout, probs, cfuncs = actions[0], actions[1], [tup[0] * pratio for tup in actions[2:]], [tup[1] for tup in actions[2:]] - # create cumulative probabilies for the random actions + # create cumulative probabilies for the random actions cprobs = [sum(v for i,v in enumerate(probs) if i<=k) for k in range(len(probs))] # rebuild a new, optimized action structure actions = (flogin, flogout, zip(cprobs, cfuncs)) - + # setting up all clients (they are automatically started) factory = DummyFactory(actions, timestep, verbose) for i in range(nclients): reactor.connectTCP("localhost", telnet_port, factory) - # start reactor + # start reactor reactor.run() #------------------------------------------------------------ -# Command line interface +# Command line interface #------------------------------------------------------------ if __name__ == '__main__': # parsing command line with default vals - parser = OptionParser(usage="%prog [options] [timestep, [port]]", + parser = OptionParser(usage="%prog [options] [timestep, [port]]", description="This program requires some preparations to run properly. Start it without any arguments or options for full help.") - parser.add_option('-v', '--verbose', action='store_const', const=1, dest='verbose', + parser.add_option('-v', '--verbose', action='store_const', const=1, dest='verbose', default=0,help="echo brief description of what clients do every timestep.") - parser.add_option('-V', '--very-verbose', action='store_const',const=2, dest='verbose', + parser.add_option('-V', '--very-verbose', action='store_const',const=2, dest='verbose', default=0,help="echo all client returns to stdout (hint: use only with nclients=1!)") - options, args = parser.parse_args() + options, args = parser.parse_args() nargs = len(args) nclients = DEFAULT_NCLIENTS timestep = DEFAULT_TIMESTEP port = DEFAULT_PORT try: - if not args : raise Exception + if not args : raise Exception if nargs > 0: nclients = max(1, int(args[0])) if nargs > 1: timestep = max(1, int(args[1])) if nargs > 2: port = int(args[2]) except Exception: print HELPTEXT - sys.exit() + sys.exit() # import the ACTION tuple from a given module - try: + try: action_modpath = settings.DUMMYRUNNER_ACTIONS_MODULE except AttributeError: # use default action_modpath = "src.utils.dummyrunner_actions" - actions = utils.mod_import(action_modpath, "ACTIONS") - + actions = utils.variable_from_module(action_modpath, "ACTIONS") + print "Connecting %i dummy client(s) to port %i using a %i second timestep ... " % (nclients, port, timestep) - t0 = time.time() - start_all_dummy_clients(actions, nclients, timestep, port, + t0 = time.time() + start_all_dummy_clients(actions, nclients, timestep, port, verbose=options.verbose) ttot = time.time() - t0 print "... dummy client runner finished after %i seconds." % ttot diff --git a/src/utils/utils.py b/src/utils/utils.py index 7c135fdbc..758b715b7 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -7,7 +7,7 @@ be of use when designing your own game. """ from inspect import ismodule -import os, sys, imp +import os, sys, imp, types import textwrap import datetime import random @@ -538,11 +538,19 @@ def has_parent(basepath, obj): # instance. Not sure if one should defend against this. return False -def mod_import(mod_path, propname=None): +def mod_import(module): """ - Takes filename of a module (a python path or a full pathname) - and imports it. If property is given, return the named - property from this module instead of the module itself. + A generic Python module loader. + + Args: + module - this can be either a Python path (dot-notation like src.objects.models), + an absolute path (e.g. /home/eve/evennia/src/objects.models.py) + or an already import module object (e.g. models) + Returns: + an imported module. If the input argument was already a model, this is returned as-is, + otherwise the path is parsed and imported. + Error: + returns None. The error is also logged. """ def log_trace(errmsg=None): @@ -567,74 +575,83 @@ def mod_import(mod_path, propname=None): for line in errmsg.splitlines(): log.msg('[EE] %s' % line) - if not mod_path: + if not module: return None - # first try to import as a python path - try: - mod = __import__(mod_path, fromlist=["None"]) - except ImportError: - - # try absolute path import instead - - if not os.path.isabs(mod_path): - mod_path = os.path.abspath(mod_path) - path, filename = mod_path.rsplit(os.path.sep, 1) - modname = filename.rstrip('.py') + if type(module) == types.ModuleType: + # if this is already a module, we are done + mod = module + else: + # first try to import as a python path try: - result = imp.find_module(modname, [path]) + mod = __import__(module, fromlist=["None"]) except ImportError: - log_trace("Could not find module '%s' (%s.py) at path '%s'" % (modname, modname, path)) - return - try: - mod = imp.load_module(modname, *result) - except ImportError: - log_trace("Could not find or import module %s at path '%s'" % (modname, path)) - mod = None - # we have to close the file handle manually - result[0].close() - if mod and propname: - # we have a module, extract the sought property from it. - try: - mod_prop = mod.__dict__[to_str(propname)] - except KeyError: - log_trace("Could not import property '%s' from module %s." % (propname, mod_path)) - return None - return mod_prop + # try absolute path import instead + + if not os.path.isabs(module): + module = os.path.abspath(module) + path, filename = module.rsplit(os.path.sep, 1) + modname = filename.rstrip('.py') + + try: + result = imp.find_module(modname, [path]) + except ImportError: + log_trace("Could not find module '%s' (%s.py) at path '%s'" % (modname, modname, path)) + return + try: + mod = imp.load_module(modname, *result) + except ImportError: + log_trace("Could not find or import module %s at path '%s'" % (modname, path)) + mod = None + # we have to close the file handle manually + result[0].close() return mod -def variable_from_module(modpath, variable=None, default=None): +def variable_from_module(module, variable=None, default=None): """ - Retrieve a variable from a module. The variable must be defined - globally in the module. If no variable is given, a random variable - is returned from the module. + Retrieve a variable or list of variables from a module. The variable(s) must be defined + globally in the module. If no variable is given (or a list entry is None), a random variable + is extracted from the module. If module cannot be imported or given variable not found, default is returned. - """ - if not modpath: - return default - try: - mod = __import__(modpath, fromlist=["None"]) - except ImportError: - return default - if variable: - # try to pick a named variable - return mod.__dict__.get(variable, default) - else: - # random selection - mvars = [val for key, val in mod.__dict__.items() if not (key.startswith("_") or ismodule(val))] - return mvars and random.choice(mvars) -def string_from_module(modpath, variable=None, default=None): + Args: + module (string or module)- python path, absolute path or a module + variable (string or iterable) - single variable name or iterable of variable names to extract + default (string) - default value to use if a variable fails to be extracted. + Returns: + a single value or a list of values depending on the type of 'variable' argument. Errors in lists + are replaced by the 'default' argument.""" + + if not module: + return default + mod = mod_import(module) + + result = [] + for var in make_iter(variable): + if var: + # try to pick a named variable + result.append(mod.__dict__.get(var, default)) + else: + # random selection + mvars = [val for key, val in mod.__dict__.items() if not (key.startswith("_") or ismodule(val))] + result.append((mvars and random.choice(mvars)) or default) + if len(result) == 1: + return result[0] + return result + +def string_from_module(module, variable=None, default=None): """ This is a wrapper for variable_from_module that requires return value to be a string to pass. It's primarily used by login screen. """ - val = variable_from_module(modpath, variable=variable, default=default) + val = variable_from_module(module, variable=variable, default=default) if isinstance(val, basestring): return val + elif is_iter(val): + return [(isinstance(v, basestring) and v or default) for v in val] return default def init_new_player(player):