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
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
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