Exemple #1
0
def task(cls):
    """
    A decorator which turns a class into a Zookeeper task, which is a Zookeeper
    method with an argument-less `run` method.

    Tasks are runnable through the CLI. Upon execution, the task is instantiated
    and all component fields are configured using configuration passed as CLI
    arguments of the form `field_name=field_value`, and then the `run` method is
    called.
    """
    cls = component(cls)

    if not (hasattr(cls, "run") and callable(cls.run)):
        raise TypeError("Classes decorated with @task must define a `run` method.")

    # Enforce argument-less `run`.

    call_args = inspect.signature(cls.run).parameters
    if len(call_args) > 1 or len(call_args) == 1 and "self" not in call_args:
        raise TypeError(
            "A @task class must define a `run` method taking no arguments except "
            f"`self`, which runs the task, but `{cls.__name__}.run` accepts arguments "
            f"{tuple(name for name in call_args)}."
        )

    # Register a CLI command to run the task.

    if convert_to_snake_case(cls.__name__) in (
        convert_to_snake_case(c) for c in cli.commands
    ):
        raise ValueError(
            f"Task naming conflict. Task with name '{cls.__name__}' (or similar) "
            "already registered. Note that the task name is the name of the class that "
            "the @task decorator is applied to."
        )

    @cli.command(cls.__name__, context_settings=dict(ignore_unknown_options=True))
    @click.option(
        "-i",
        "--interactive",
        is_flag=True,
        default=False,
        help="Interactively configure task.",
    )
    @click.argument("config", type=ConfigParam(), nargs=-1)
    def command(config, interactive):
        config = {k: v for k, v in config}
        task_instance = cls()
        configure(task_instance, config, interactive=interactive)
        task_instance.run()

    return cls
Exemple #2
0
def task(cls):
    """
    A decorator which turns a class into a Zookeeper task, which is a Zookeeper
    method with an argument-less `run` method.

    Tasks are runnable through the CLI. Upon execution, the task is instantiated
    and all component fields are configured using configuration passed as CLI
    arguments of the form `field_name=field_value`, and then the `run` method is
    called.
    """

    cls = component(cls)

    if not (hasattr(cls, "run") and callable(cls.run)):
        raise ValueError("Classes decorated with @task must define a `run` method.")

    # Enforce argument-less `run`

    call_args = inspect.signature(cls.run).parameters
    if len(call_args) > 1 or len(call_args) == 1 and "self" not in call_args:
        raise ValueError(
            "A task class must define a `run` method taking no arguments except "
            f"`self`, which runs the task, but `{cls.__name__}.run` accepts arguments "
            f"{call_args}."
        )

    # Register a CLI command to run the task.

    task_name = convert_to_snake_case(cls.__name__)
    if task_name in cli.commands:
        raise ValueError(
            f"Task naming conflict. Task with name '{task_name}' already registered. "
            "Note that the task name is the name of the class that the @task decorator "
            "is applied to, normalised to 'snake case', e.g. 'FooBarTask' -> "
            "'foo_bar_task'."
        )

    @cli.command(task_name)
    @click.argument("config", type=ConfigParam(), nargs=-1)
    @click.option("-i", "--interactive", is_flag=True, default=False)
    def command(config, interactive):
        config = {k: v for k, v in config}
        task_instance = cls()
        configure(task_instance, config, interactive=interactive)
        task_instance.run()

    return cls
Exemple #3
0
def factory(cls: Type):
    """
    A decorator which turns a class into a Zookeeper factory.

    Factories are in particular Zookeeper components, so can have `Field`s and
    `ComponentFields`. Factories must define an argument-less `build()` method,
    with a return type annotation.

    When a factory component is used as a sub-component (i.e., configured as the
    value of a `ComponentField` in some parent component instance), the
    `build()` method is implicitly called upon the first access of the field
    value, and the result of `build()` is used as the value in the parent.

    Here is an example:

    ```
    @factory
    class F:
        a: int = Field()
        def build(self):
            return self.a + 4

    @component
    class C:
        a: int = Field(3)
        f: int = ComponentField(F)

    print(C().f)

    >> # Output
    >> 7
    ```
    """
    cls = component(cls)

    try:
        signature = inspect.signature(cls.build)
        params = signature.parameters
        if (len(params) != 1 or "self" not in params or list(
                params.values())[0].kind in (inspect.Parameter.VAR_POSITIONAL,
                                             inspect.Parameter.VAR_KEYWORD)):
            raise TypeError()
    except (AttributeError, TypeError):
        raise TypeError(
            "Classes decorated with @factory must implement a `build()` method taking "
            "precisely one positional argument, `self`.") from None

    if signature.return_annotation is signature.empty:
        raise TypeError(
            "The `build()` method of a @factory class must have an annotated return "
            "type annotation, e.g.:\n\n"
            "```\n"
            "@factory\n"
            "class MyFactory:\n"
            "    ...\n"
            "    def build(self) -> SomeReturnType:\n"
            "        ...\n"
            "        return some_value\n"
            "```")

    cls.__component_factory_return_type__ = signature.return_annotation
    cls.__component_factory_value__ = utils.missing

    _wrap_build(cls)
    _wrap_str_repr(cls)

    if signature.return_annotation not in FACTORY_REGISTRY:
        FACTORY_REGISTRY[signature.return_annotation] = set([cls])
    else:
        FACTORY_REGISTRY[signature.return_annotation].add(cls)

    return cls