diff --git a/backtrader/errors.py b/backtrader/errors.py index f702a43fc..d6d875c4c 100644 --- a/backtrader/errors.py +++ b/backtrader/errors.py @@ -34,3 +34,18 @@ class StrategySkipError(BacktraderError): '''Requests the platform to skip this strategy for backtesting. To be raised during the initialization (``__init__``) phase of the instance''' pass + + +class ModuleImportError(BacktraderError): + '''Raised if a class requests a module to be present to work and it cannot + be imported''' + def __init__(self, message, *args): + super(ModuleImportError, self).__init__(message) + self.args = args + + +class FromModuleImportError(ModuleImportError): + '''Raised if a class requests a module to be present to work and it cannot + be imported''' + def __init__(self, message, *args): + super(FromModuleImportError, self).__init__(message, *args) diff --git a/backtrader/metabase.py b/backtrader/metabase.py index 8e66e67f7..36482869d 100644 --- a/backtrader/metabase.py +++ b/backtrader/metabase.py @@ -25,7 +25,8 @@ import itertools import sys -from .utils.py3 import zip +import backtrader as bt +from .utils.py3 import zip, string_types def findbases(kls, topclass): @@ -205,21 +206,68 @@ def __new__(meta, name, bases, dct): # (and hence "repetition") newparams = dct.pop('params', ()) + packs = 'packages' + newpackages = tuple(dct.pop(packs, ())) # remove before creation + + fpacks = 'frompackages' + fnewpackages = tuple(dct.pop(fpacks, ())) # remove before creation + # Create the new class - this pulls predefined "params" cls = super(MetaParams, meta).__new__(meta, name, bases, dct) # Pulls the param class out of it - default is the empty class params = getattr(cls, 'params', AutoInfoClass) + # Pulls the packages class out of it - default is the empty class + packages = tuple(getattr(cls, packs, ())) + fpackages = tuple(getattr(cls, fpacks, ())) + # get extra (to the right) base classes which have a param attribute morebasesparams = [x.params for x in bases[1:] if hasattr(x, 'params')] + # Get extra packages, add them to the packages and put all in the class + for y in [x.packages for x in bases[1:] if hasattr(x, packs)]: + packages += tuple(y) + + for y in [x.frompackages for x in bases[1:] if hasattr(x, fpacks)]: + fpackages += tuple(y) + + cls.packages = packages + newpackages + cls.frompackages = fpackages + fnewpackages + # Subclass and store the newly derived params class cls.params = params._derive(name, newparams, morebasesparams) return cls def donew(cls, *args, **kwargs): + clsmod = sys.modules[cls.__module__] + # import specified packages + for p in cls.packages: + if isinstance(p, (tuple, list)): + p, palias = p + else: + palias = p + + pmod = __import__(p) + setattr(clsmod, palias, pmod) + + # import from specified packages - the 2nd part is a string or iterable + for p, frompackage in cls.frompackages: + if isinstance(frompackage, string_types): + frompackage = (frompackage,) # make it a tuple + + for fp in frompackage: + if isinstance(fp, (tuple, list)): + fp, falias = fp + else: + fp, falias = fp, fp # assumed is string + + # complain "not string" without fp (unicode vs bytes) + pmod = __import__(p, fromlist=[str(fp)]) + pattr = getattr(pmod, fp) + setattr(clsmod, falias, pattr) + # Create params and set the values from the kwargs params = cls.params() for pname, pdef in cls.params._getitems():