From c4919428717f62c3b1c6e74f8090d2e0e8b2320e Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Sun, 29 Aug 2021 11:37:45 +0200 Subject: [PATCH] #66: Improve docs --- docs/source/index.rst | 1 + docs/source/syntax.rst | 2 + docs/source/writing-tasks.rst | 107 +++++++++++++++++++++++++++ src/core/rkd/core/api/contract.py | 69 ++++++++++++++--- src/core/rkd/core/standardlib/env.py | 3 +- 5 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 docs/source/writing-tasks.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 106cb1a3..4040d7d7 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -101,6 +101,7 @@ Keep learning project-structure standardlib/index environment + writing-tasks usage/index rts/index diff --git a/docs/source/syntax.rst b/docs/source/syntax.rst index 8bad0c56..341624fc 100644 --- a/docs/source/syntax.rst +++ b/docs/source/syntax.rst @@ -1,3 +1,5 @@ +.. _syntax: + Syntax ====== diff --git a/docs/source/writing-tasks.rst b/docs/source/writing-tasks.rst new file mode 100644 index 00000000..a9b56109 --- /dev/null +++ b/docs/source/writing-tasks.rst @@ -0,0 +1,107 @@ +Writing reusable tasks +====================== + +There are different ways to achieve similar goal, to define the Task. In chapter about :ref:`syntax` you can learn differences +between those multiple ways. + +Now we will focus on **Classic Python** syntax which allows to define Tasks as classes, those classes can be packaged +into Python packages and reused across projects and event organizations. + + +Importing packages +------------------ + +Everytime a new project is created there is no need to duplicate same solutions over and over again. +Even in simplest makefiles there are ready-to-use tasks from :code:`rkd.core.standardlib` imported and used. + +.. code:: yaml + + version: org.riotkit.rkd/yaml/v2 + imports: + - my_org.my_package1 + + +Package index +------------- + +A makefile can import a class or whole package. There is no any automatic class discovery, every package exports what was intended to export. + +Below is explained how does it work that Makefile can import multiple tasks from :code:`my_org.my_package1` without specifying classes one-by-one. + +**Example package structure** + +.. code:: bash + + my_package1/ + my_package1/__init__.py + my_package1/script.py + my_package1/composer.py + + +**Example __init__.py inside Python package e.g. my_org.my_package1** + +.. code:: python + + from rkd.core.api.syntax import TaskDeclaration + from .composer import ComposerIntegrationTask # (1) + from .script import PhpScriptTask, imports as script_imports # (2) + + + # (3) + def imports(): + return [ + TaskDeclaration(ComposerIntegrationTask()) # (5) + ] + script_imports() # (4) + + +- (1): **ComposerIntegrationTask** was imported from **composer.py** file +- (2): **imports as script_imports** other **def imports()** from **script.py** was loaded and used in **(4)** +- (3): **def imports()** defines which tasks will appear automatically in your build, when you import whole module, not a single class +- (5): **TaskDeclaration** can decide about custom task name, custom working directory, if the task is **internal** which means - if should be listed on :tasks + + +Task construction +----------------- + +Basic example of how the Task looks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. literalinclude:: ../../src/core/rkd/core/standardlib/env.py + :start-after: + :end-before: # + +Basic configuration methods to implement +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- **get_name():** Define a name e.g. :code:`:my-task` +- **get_group_name():** Optionally a group name e.g. :code:`:app1` +- **get_declared_envs():** List of allowed environment variables to be used inside of this Task +- **configure_argparse():** Commandline switches configuration, uses Python's native ArgParse +- **get_configuration_attributes()**: Optionally. If our Task is designed to be used as Base Task of other Task, then there we can limit which methods and class attributes can be called from **configure()** method + +.. autoclass:: rkd.core.api.contract.TaskInterface + :members: get_name, get_group_name, get_declared_envs, configure_argparse, get_configuration_attributes + + +Basic action methods +~~~~~~~~~~~~~~~~~~~~ + +- **execute():** Contains the Task logic, there is access to environment variables, commandline switches and class attributes +- **inner_execute():** If you want to create a Base Task, then implement a call to this method inside **execute()**, so the Task that extends your Base Task can inject code inside **execute()** you defined +- **configure():** If our Task extends other Task, then there is a possibility to configure Base Task in this method +- **compile():** Code that will execute on compilation stage. There is an access to **CompilationLifecycleEvent** which allows several operations such as **task expansion** (converting current task into a Pipeline with dynamically created Tasks) + +.. autoclass:: rkd.core.api.contract.ExtendableTaskInterface + :members: execute, configure, compile, inner_execute + + +Additional methods that can be called inside execute() and inner_execute() +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- **io():** Provides logging inside **execute()** and **configure()** +- **rkd() and sh():** Executes commands in subshells +- **py():** Executes Python code isolated in a subshell + +.. autoclass:: rkd.core.api.contract.ExtendableTaskInterface + :members: io, rkd, sh, py + diff --git a/src/core/rkd/core/api/contract.py b/src/core/rkd/core/api/contract.py index 8a32d919..8b0a641f 100644 --- a/src/core/rkd/core/api/contract.py +++ b/src/core/rkd/core/api/contract.py @@ -320,25 +320,31 @@ def copy_internal_dependencies(self, task): @abstractmethod def get_name(self) -> str: - """Task name eg. ":sh" + """ + Task name eg. ":sh" """ pass @abstractmethod def get_group_name(self) -> str: - """Group name where the task belongs eg. ":publishing", can be empty. + """ + Group name where the task belongs eg. ":publishing", can be empty. """ pass def get_become_as(self) -> str: - """User name in UNIX/Linux system, optional. - When defined, then current task will be executed as this user (WARNING: a forked process would be started)""" + """ + User name in UNIX/Linux system, optional. + When defined, then current task will be executed as this user (WARNING: a forked process would be started) + """ return '' def should_fork(self) -> bool: - """Decides if task should be ran in a separate Python process (be careful with it)""" + """ + Decides if task should be ran in a separate Python process (be careful with it) + """ return self.get_become_as() != '' @@ -349,25 +355,51 @@ def get_description(self) -> str: @abstractmethod def execute(self, context: ExecutionContext) -> bool: - """ Executes a task. True/False should be returned as return """ + """ + Executes a task. True/False should be returned as return + """ pass @abstractmethod def configure_argparse(self, parser: ArgumentParser): - """ Allows a task to configure ArgumentParser (argparse) """ + """ + Allows a task to configure ArgumentParser (argparse) + + .. code:: python + + def configure_argparse(self, parser: ArgumentParser): + parser.add_argument('--php', help='PHP version ("php" docker image tag)', default='8.0-alpine') + parser.add_argument('--image', help='Docker image name', default='php') + """ pass # ====== LIFECYCLE METHODS ENDS def get_full_name(self): - """ Returns task full name, including group name """ + """ + Returns task full name, including group name + """ return self.get_group_name() + self.get_name() @classmethod def get_declared_envs(cls) -> Dict[str, Union[str, ArgumentEnv]]: - """ Dictionary of allowed envs to override: KEY -> DEFAULT VALUE """ + """ + Dictionary of allowed envs to override: KEY -> DEFAULT VALUE + + All environment variables fetched from the ExecutionContext needs to be defined there. + Declared values there are automatically documented in --help + + .. code:: python + + @classmethod + def get_declared_envs(cls) -> Dict[str, Union[str, ArgumentEnv]]: + return { + 'PHP': ArgumentEnv('PHP', '--php', '8.0-alpine'), + 'IMAGE': ArgumentEnv('IMAGE', '--image', 'php') + } + """ return {} def internal_normalized_get_declared_envs(self) -> Dict[str, ArgumentEnv]: @@ -486,8 +518,6 @@ def silent_sh(self, cmd: str, verbose: bool = False, strict: bool = True, return super().silent_sh(cmd=cmd, verbose=verbose, strict=strict, env=env) def __str__(self): - - return 'Task<{name}, object_id={id}, extended_from={extends}>'.format( name=self.get_full_name(), id=id(self), @@ -531,6 +561,12 @@ def is_internal(self) -> bool: return False def extends_task(self): + """ + Provides information if this Task has a Parent Task + + :return: + """ + try: extends_from = self._extended_from.__module__ + '.' + self._extended_from.__name__ except AttributeError: @@ -551,6 +587,17 @@ def __deepcopy__(self, memodict={}): class ExtendableTaskInterface(TaskInterface, ABC): def inner_execute(self, ctx: ExecutionContext) -> bool: + """ + Method that can be executed inside execute() - if implemented. + + Use cases: + - Allow child Task to inject code between e.g. database startup and database shutdown to execute some + operations on the database + + :param ctx: + :return: + """ + pass def get_configuration_attributes(self) -> List[str]: diff --git a/src/core/rkd/core/standardlib/env.py b/src/core/rkd/core/standardlib/env.py index 5bcabeef..416cd032 100644 --- a/src/core/rkd/core/standardlib/env.py +++ b/src/core/rkd/core/standardlib/env.py @@ -6,6 +6,7 @@ from ..api.contract import TaskInterface, ExecutionContext +# class GetEnvTask(TaskInterface): """Gets environment variable value""" @@ -19,10 +20,10 @@ def configure_argparse(self, parser: ArgumentParser): parser.add_argument('--name', '-e', help='Environment variable name', required=True) def execute(self, context: ExecutionContext) -> bool: - # @todo: test for case, when None then '' self.io().out(os.getenv(context.get_arg('--name'), '')) return True +# class SetEnvTask(TaskInterface):