diff --git a/CHANGELOG.md b/CHANGELOG.md index e619cf2..e9fa7a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- `ewokstasks` directive displays task tags. + ## [2.2.1] - 2026-04-21 ### Fixed diff --git a/doc/examples.md b/doc/examples.md index cecb4a3..37356e0 100644 --- a/doc/examples.md +++ b/doc/examples.md @@ -32,6 +32,37 @@ :::: +Class tasks can be classified by using the [`tags` class argument](https://ewokscore.readthedocs.io/en/latest/howtoguides/task_discovery.html#how-to-tag-tasks). These tags will also be displayed if present + +::::{tab-set} + +:::{tab-item} Python + +```{literalinclude} ../src/ewokssphinx/tests/dummy_tasks_tags.py +:language: python +``` + +::: + +:::{tab-item} reST + +```rst +.. ewokstasks:: ewokssphinx.tests.dummy_tasks_tags + :task-type: class +``` + +::: + +:::{tab-item} Sphinx + +```{ewokstasks} ewokssphinx.tests.dummy_tasks_tags +:task-type: class +``` + +::: + +:::: + ## Class with pydantic model @@ -119,6 +150,8 @@ If the task pydantic models contain themselves pydantic models, the nested model :::: + + ## Method ::::{tab-set} diff --git a/pyproject.toml b/pyproject.toml index 12ef45e..ae36f91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ requires-python = ">=3.10" dependencies = [ "sphinx", - "ewokscore >=3.1.0", + "ewokscore >=5.1.0rc6", "pydantic", ] diff --git a/src/ewokssphinx/ewoks_task_utils.py b/src/ewokssphinx/ewoks_task_utils.py index 42ae019..2ac1491 100644 --- a/src/ewokssphinx/ewoks_task_utils.py +++ b/src/ewokssphinx/ewoks_task_utils.py @@ -60,6 +60,7 @@ def discover_tasks( "inputs": inputs, "outputs": outputs, "submodels": {k: _parse_pydantic_model(v) for k, v in submodels.items()}, + "tags": task.get("tags", []), } _ = TaskDescription(**serialized_task) diff --git a/src/ewokssphinx/ewokssphinx.css b/src/ewokssphinx/ewokssphinx.css index 5d48c31..adb2d77 100644 --- a/src/ewokssphinx/ewokssphinx.css +++ b/src/ewokssphinx/ewokssphinx.css @@ -23,3 +23,8 @@ .ewokssphinx-task .ewokssphinx-examples li>div { margin: 0; } + +.ewokssphinx-task .ewokssphinx-tag { + border-radius: 0.75em; + padding: 0.1em 0.6em; +} diff --git a/src/ewokssphinx/sphinx_task_utils.py b/src/ewokssphinx/sphinx_task_utils.py index d25219c..153f811 100644 --- a/src/ewokssphinx/sphinx_task_utils.py +++ b/src/ewokssphinx/sphinx_task_utils.py @@ -38,14 +38,15 @@ def _task_section(directive: SphinxDirective, task: dict[str, Any]) -> nodes.sec if task["description"]: section.extend(_rst_to_nodes(directive, task["description"])) - # Identifier + task type - section.append( - nodes.field_list( - "", - _field_node("Identifier", nodes.literal(text=task["task_identifier"])), - _field_node("Task type", nodes.Text(task["task_type"])), - ) - ) + # Identifier + task type + tags + fields = [ + _field_node("Identifier", nodes.literal(text=task["task_identifier"])), + _field_node("Task type", nodes.Text(task["task_type"])), + ] + tags = task.get("tags") + if tags: + fields.append(_field_node("Tags", _tags_node(tags))) + section.append(nodes.field_list("", *fields)) section.append( _field_list(directive, {"inputs": task["inputs"], "outputs": task["outputs"]}) @@ -89,6 +90,14 @@ def _field_node(name: str, body: nodes.Element) -> nodes.field: ) +def _tags_node(tags: list[str]) -> nodes.inline: + tag_nodes = [] + for tag in tags: + tag_nodes.append(nodes.literal(text=tag, classes=["ewokssphinx-tag"])) + tag_nodes.append(nodes.Text(" ")) + return nodes.inline("", "", *tag_nodes[:-1], classes=["ewokssphinx-tags"]) + + def _term_node(name: str, is_required: bool) -> nodes.term: """ Create a term: diff --git a/src/ewokssphinx/tests/dummy_tasks_tags.py b/src/ewokssphinx/tests/dummy_tasks_tags.py new file mode 100644 index 0000000..830396b --- /dev/null +++ b/src/ewokssphinx/tests/dummy_tasks_tags.py @@ -0,0 +1,22 @@ +from ewokscore import Task + + +class XrfTask(Task, output_names=["result"], tags=["xrf", "imaging"]): + """XRF task""" + + def run(self): + pass + + +class XasTask(Task, output_names=["result"], tags=["xas"]): + """XAS task""" + + def run(self): + pass + + +class UntaggedTask(Task, output_names=["result"]): + """Untagged task""" + + def run(self): + pass diff --git a/src/ewokssphinx/tests/test_task_tags.py b/src/ewokssphinx/tests/test_task_tags.py new file mode 100644 index 0000000..32f9a97 --- /dev/null +++ b/src/ewokssphinx/tests/test_task_tags.py @@ -0,0 +1,30 @@ +from docutils import nodes +from sphinx.testing import restructuredtext + + +def test_ewokstasks_tags(app): + parsed_nodes = restructuredtext.parse( + app, + """.. ewokstasks:: ewokssphinx.tests.dummy_tasks_tags + :task-type: class + """, + ) + + assert _task_tags(parsed_nodes) == { + "XrfTask": ["imaging", "xrf"], + "XasTask": ["xas"], + "UntaggedTask": [], + } + + +def _task_tags(document: nodes.document) -> dict[str, list[str]]: + """Map task names to their tags.""" + tags = {} + for field_list in document.findall(nodes.field_list): + task_name = field_list.parent.next_node(nodes.title).astext() + tags[task_name] = [ + node.astext() + for node in field_list.findall(nodes.literal) + if "ewokssphinx-tag" in node["classes"] + ] + return tags diff --git a/src/ewokssphinx/type_utils.py b/src/ewokssphinx/type_utils.py index 20ee92f..b36b4c8 100644 --- a/src/ewokssphinx/type_utils.py +++ b/src/ewokssphinx/type_utils.py @@ -21,6 +21,7 @@ class TaskDescription(BaseModel, extra="forbid"): inputs: list[TaskParameter] outputs: list[TaskParameter] submodels: dict[str, list[TaskParameter]] + tags: list[str] = [] ParameterDescription = dict[str, str | list[Any] | bool | None]