src/sisy/models.py
def func_from_string(callable_str):
    """Return a live function from a full dotted path.  Must be either a plain function
    directly in a module, a class function, or a static function.  (No modules, classes,
    or instance methods, since those can't be called as tasks.)"""
    components = callable_str.split('.')

    func = None

    if len(components) < 2:
        raise ValueError("Need full dotted path to task function")
    elif len(components) == 2:
        mod_name = components[0]
        func_name = components[1]
        try:
            mod = import_module(mod_name)
        except ModuleNotFoundError:
            raise ValueError(f"Module {mod_name} not found")
        func = get_member(mod, func_name)
        if func is None:
            raise ValueError(f"{func_name} is not a member of {mod_name}")
    else:
        mod_name = '.'.join(components[:-1])
        func_name = components[-1]

        try:
            mod = import_module(mod_name)
        except ModuleNotFoundError:
            mod_name = '.'.join(components[:-2])
            class_name = components[-2]

            try:
                mod = import_module(mod_name)
            except ModuleNotFoundError:
                raise ValueError(f"Module {mod_name} not found")

            klass = get_member(mod, class_name)
            if klass is None:
                raise ValueError(f"Class {class_name} is not a member of {mod_name}")

            func = get_member(klass, func_name)
            if func is None:
                raise ValueError(f"Function {func_name} is not a member of {mod_name}.{class_name}")

        if func is None:
            func = get_member(mod, func_name)
            if func is None:
                raise ValueError(f"Function {func_name} is not a member of {mod_name}")

    if inspect.ismodule(func):
        raise ValueError("Cannot call module directly")

    if inspect.isclass(func):
        raise ValueError("Cannot call class directly")

    try:
        sig = [x for x in inspect.signature(func).parameters]
    except TypeError:
        raise ValueError(f"{callable_str} ({str(type(func))[1:-1]}) is not a callable object")

    if len(sig) == 1:
        if sig[0] == 'message':
            return func
        else:
            raise ValueError("Task function must have one parameter, named 'message'")

    elif len(sig)==2 and sig[0]=='self' and sig[1]=='message':
        # We only check for the conventional 'self', but if you're using something else,
        # you deserve the pain you'll have trying to debug this.
        raise ValueError("Can't call instance method without an instance! (Try sisy.models.task_with_callable)")
    else:
        raise ValueError("Improper signature for task function (needs only 'message')")
def func_from_string(callable_str):
    """Return a live function from a full dotted path.  Must be either a plain function
    directly in a module, a class function, or a static function.  (No modules, classes,
    or instance methods, since those can't be called as tasks.)"""
    components = callable_str.split('.')

    func = None

    if len(components) < 2:
        raise ValueError("Need full dotted path to task function")
    elif len(components) == 2:
        mod_name = components[0]
        func_name = components[1]
        try:
            mod = import_module(mod_name)
        except ModuleNotFoundError:
            raise ValueError(f"Module {mod_name} not found")
        func = get_member(mod, func_name)
        if func is None:
            raise ValueError(f"{func_name} is not a member of {mod_name}")
    else:
        mod_name = '.'.join(components[:-1])
        func_name = components[-1]

        try:
            mod = import_module(mod_name)
        except ModuleNotFoundError:
            mod_name = '.'.join(components[:-2])
            class_name = components[-2]

            try:
                mod = import_module(mod_name)
            except ModuleNotFoundError:
                raise ValueError(f"Module {mod_name} not found")

            klass = get_member(mod, class_name)
            if klass is None:
                raise ValueError(f"Class {class_name} is not a member of {mod_name}")

            func = get_member(klass, func_name)
            if func is None:
                raise ValueError(f"Function {func_name} is not a member of {mod_name}.{class_name}")

        if func is None:
            func = get_member(mod, func_name)
            if func is None:
                raise ValueError(f"Function {func_name} is not a member of {mod_name}")

    if inspect.ismodule(func):
        raise ValueError("Cannot call module directly")

    if inspect.isclass(func):
        raise ValueError("Cannot call class directly")

    try:
        sig = [x for x in inspect.signature(func).parameters]
    except TypeError:
        raise ValueError(f"{callable_str} ({str(type(func))[1:-1]}) is not a callable object")

    if len(sig) == 1:
        if sig[0] == 'message':
            return func
        else:
            raise ValueError("Task function must have one parameter, named 'message'")

    elif len(sig)==2 and sig[0]=='self' and sig[1]=='message':
        # We only check for the conventional 'self', but if you're using something else,
        # you deserve the pain you'll have trying to debug this.
        raise ValueError("Can't call instance method without an instance! (Try sisy.models.task_with_callable)")
    else:
        raise ValueError("Improper signature for task function (needs only 'message')")
Return a live function from a full dotted path.  Must be either a plain function
    directly in a module, a class function, or a static function.  (No modules, classes,
    or instance methods, since those can't be called as tasks.)