ewokssphinx 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ewokssphinx/__init__.py +44 -0
- ewokssphinx/ewoks_task_directive.py +45 -0
- ewokssphinx/ewoks_task_utils.py +144 -0
- ewokssphinx/ewokssphinx.css +16 -0
- ewokssphinx/sphinx_task_utils.py +192 -0
- ewokssphinx/tests/__init__.py +0 -0
- ewokssphinx/tests/conftest.py +109 -0
- ewokssphinx/tests/dummy_tasks.py +33 -0
- ewokssphinx/tests/dummy_tasks_pydantic.py +30 -0
- ewokssphinx/tests/test_task_cache.py +26 -0
- ewokssphinx/tests/test_task_directive.py +117 -0
- ewokssphinx/tests/test_task_directive_pydantic.py +59 -0
- ewokssphinx/tests/test_utils.py +71 -0
- ewokssphinx/type_utils.py +20 -0
- ewokssphinx-2.1.0.dist-info/METADATA +88 -0
- ewokssphinx-2.1.0.dist-info/RECORD +19 -0
- ewokssphinx-2.1.0.dist-info/WHEEL +5 -0
- ewokssphinx-2.1.0.dist-info/licenses/LICENSE.md +20 -0
- ewokssphinx-2.1.0.dist-info/top_level.txt +1 -0
ewokssphinx/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from sphinx.application import Sphinx
|
|
4
|
+
from sphinx.util.osutil import copyfile
|
|
5
|
+
|
|
6
|
+
from .ewoks_task_directive import EwoksTaskDirective
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def setup(app: Sphinx):
|
|
10
|
+
app.add_directive("ewokstasks", EwoksTaskDirective)
|
|
11
|
+
|
|
12
|
+
app.add_config_value(
|
|
13
|
+
"ewokssphinx_task_cache_path",
|
|
14
|
+
default=None,
|
|
15
|
+
types=str,
|
|
16
|
+
rebuild="env",
|
|
17
|
+
description="JSON path to cache Ewoks task descriptions. "
|
|
18
|
+
"Task discovery overrides the cached descriptions. "
|
|
19
|
+
"Useful when task discovery needs to be done in different python environments.",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
app.add_config_value(
|
|
23
|
+
"ewokssphinx_ignore_discovery_error",
|
|
24
|
+
default=False,
|
|
25
|
+
types=bool,
|
|
26
|
+
rebuild="env",
|
|
27
|
+
description="Ignore Ewoks task discovery errors. ",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
app.add_css_file("ewokssphinx.css")
|
|
31
|
+
app.connect("build-finished", _copy_css)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
"version": "1.0",
|
|
35
|
+
"parallel_read_safe": False, # Reading source files
|
|
36
|
+
"parallel_write_safe": True, # Writing output files
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _copy_css(app, exception):
|
|
41
|
+
if app.builder.name != "html" or exception:
|
|
42
|
+
return
|
|
43
|
+
static_dir = Path(app.builder.outdir) / "_static"
|
|
44
|
+
copyfile(Path(__file__).parent / "ewokssphinx.css", static_dir / "ewokssphinx.css")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from docutils.parsers.rst import directives
|
|
4
|
+
from sphinx.util.docutils import SphinxDirective
|
|
5
|
+
|
|
6
|
+
from .ewoks_task_utils import cached_tasks
|
|
7
|
+
from .ewoks_task_utils import discover_tasks
|
|
8
|
+
from .sphinx_task_utils import task_nodes
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _task_type_option(argument):
|
|
14
|
+
return directives.choice(argument, ("class", "method", "ppfmethod"))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EwoksTaskDirective(SphinxDirective):
|
|
18
|
+
required_arguments = 1
|
|
19
|
+
option_spec = {
|
|
20
|
+
"task-type": _task_type_option,
|
|
21
|
+
"ignore-import-error": directives.flag,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def run(self):
|
|
25
|
+
module_pattern = self.arguments[0]
|
|
26
|
+
task_type = self.options.get("task-type")
|
|
27
|
+
ignore_import_error = "ignore-import-error" in self.options
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
local_tasks = discover_tasks(module_pattern, task_type, ignore_import_error)
|
|
31
|
+
except Exception as ex:
|
|
32
|
+
if not self.config.ewokssphinx_ignore_discovery_error:
|
|
33
|
+
raise
|
|
34
|
+
logger.error(f"Task discovery '{module_pattern}' failed: {ex}")
|
|
35
|
+
local_tasks = []
|
|
36
|
+
|
|
37
|
+
tasks = cached_tasks(
|
|
38
|
+
local_tasks,
|
|
39
|
+
self.config.ewokssphinx_task_cache_path,
|
|
40
|
+
module_pattern,
|
|
41
|
+
task_type,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
task_sections = task_nodes(self, tasks)
|
|
45
|
+
return task_sections
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ewokscore.task_discovery import discover_tasks_from_modules
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from pydantic_core import PydanticUndefined
|
|
10
|
+
from sphinx.util.typing import stringify_annotation
|
|
11
|
+
|
|
12
|
+
from .type_utils import TaskDescription
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def discover_tasks(
|
|
16
|
+
module_pattern: str, task_type: str, ignore_import_error: bool
|
|
17
|
+
) -> list[dict[str, Any]]:
|
|
18
|
+
tasks = []
|
|
19
|
+
for task in discover_tasks_from_modules(
|
|
20
|
+
module_pattern,
|
|
21
|
+
task_type=task_type,
|
|
22
|
+
raise_import_failure=not ignore_import_error,
|
|
23
|
+
):
|
|
24
|
+
task_name = _get_task_name(task["task_identifier"], task["task_type"])
|
|
25
|
+
|
|
26
|
+
if task.get("input_model"):
|
|
27
|
+
inputs = _parse_pydantic_model(task["input_model"])
|
|
28
|
+
else:
|
|
29
|
+
inputs = _parse_parameter_names(
|
|
30
|
+
task.get("required_input_names", []),
|
|
31
|
+
task.get("optional_input_names", []),
|
|
32
|
+
)
|
|
33
|
+
if task.get("output_model"):
|
|
34
|
+
outputs = _parse_pydantic_model(task["output_model"])
|
|
35
|
+
else:
|
|
36
|
+
outputs = _parse_parameter_names(task.get("output_names", []), [])
|
|
37
|
+
|
|
38
|
+
serialized_task = {
|
|
39
|
+
"task_name": task_name,
|
|
40
|
+
"task_identifier": task["task_identifier"],
|
|
41
|
+
"task_type": task["task_type"],
|
|
42
|
+
"description": _parse_doc(task.get("description")),
|
|
43
|
+
"inputs": inputs,
|
|
44
|
+
"outputs": outputs,
|
|
45
|
+
}
|
|
46
|
+
_ = TaskDescription(**serialized_task)
|
|
47
|
+
|
|
48
|
+
tasks.append(serialized_task)
|
|
49
|
+
return tasks
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cached_tasks(
|
|
53
|
+
local_tasks: list[dict[str, Any]],
|
|
54
|
+
json_cache_path: str | Path | None,
|
|
55
|
+
module_pattern: str,
|
|
56
|
+
task_type: str,
|
|
57
|
+
) -> list[dict[str, Any]]:
|
|
58
|
+
"""Save tasks in cache or load task from cache when local tasks are empty."""
|
|
59
|
+
if not json_cache_path:
|
|
60
|
+
return local_tasks
|
|
61
|
+
|
|
62
|
+
json_cache_path = Path(json_cache_path)
|
|
63
|
+
if json_cache_path.exists():
|
|
64
|
+
cache = json.loads(json_cache_path.read_text(encoding="utf-8"))
|
|
65
|
+
else:
|
|
66
|
+
cache = {}
|
|
67
|
+
|
|
68
|
+
module_pattern_dict = cache.setdefault(module_pattern, {})
|
|
69
|
+
if local_tasks or task_type not in module_pattern_dict:
|
|
70
|
+
module_pattern_dict[task_type] = local_tasks
|
|
71
|
+
|
|
72
|
+
json_cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
json_cache_path.write_text(json.dumps(cache, indent=2), encoding="utf-8")
|
|
74
|
+
|
|
75
|
+
return module_pattern_dict[task_type]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_task_name(identifier: str, task_type: str) -> str:
|
|
79
|
+
if task_type == "ppfmethod":
|
|
80
|
+
# ppfmethods are all named `run` so use the module name as task name.
|
|
81
|
+
return identifier.split(".")[-2]
|
|
82
|
+
return identifier.split(".")[-1]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _parse_parameter_names(
|
|
86
|
+
required_names: list[str], optional_names: list[str]
|
|
87
|
+
) -> list[dict[str, str | list[str]]]:
|
|
88
|
+
parameters = []
|
|
89
|
+
names = required_names + optional_names
|
|
90
|
+
is_required = [True] * len(required_names) + [False] * len(optional_names)
|
|
91
|
+
for name, required in zip(names, is_required):
|
|
92
|
+
parameter = {
|
|
93
|
+
"name": name,
|
|
94
|
+
"annotation": None,
|
|
95
|
+
"required": required,
|
|
96
|
+
"description": None,
|
|
97
|
+
"examples": [],
|
|
98
|
+
"default": None,
|
|
99
|
+
"has_default": False,
|
|
100
|
+
}
|
|
101
|
+
parameters.append(parameter)
|
|
102
|
+
return parameters
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _parse_pydantic_model(model: BaseModel | None) -> list[dict[str, str | list[str]]]:
|
|
106
|
+
parameters = []
|
|
107
|
+
if model is None:
|
|
108
|
+
return parameters
|
|
109
|
+
|
|
110
|
+
model_cls = _import_model(model)
|
|
111
|
+
for name, field_info in model_cls.model_fields.items():
|
|
112
|
+
parameters.append(_parse_pydantic_field(name, field_info))
|
|
113
|
+
return parameters
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _import_model(input_model_qual_name: str) -> type:
|
|
117
|
+
module_name, _, model_name = input_model_qual_name.rpartition(".")
|
|
118
|
+
mod = importlib.import_module(module_name)
|
|
119
|
+
return getattr(mod, model_name)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _parse_doc(text) -> str:
|
|
123
|
+
return inspect.cleandoc(text) if text else ""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _parse_pydantic_field(name, field_info) -> dict[str, str | list[str] | None]:
|
|
127
|
+
examples = getattr(field_info, "examples", None)
|
|
128
|
+
default = getattr(field_info, "default", None)
|
|
129
|
+
if default is PydanticUndefined:
|
|
130
|
+
default = None
|
|
131
|
+
has_default = False
|
|
132
|
+
else:
|
|
133
|
+
has_default = True
|
|
134
|
+
examples = [repr(example) for example in examples or []]
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
"name": name,
|
|
138
|
+
"annotation": stringify_annotation(field_info.annotation),
|
|
139
|
+
"required": field_info.is_required(),
|
|
140
|
+
"description": _parse_doc(field_info.description),
|
|
141
|
+
"examples": examples,
|
|
142
|
+
"default": None if default is None else str(default),
|
|
143
|
+
"has_default": has_default,
|
|
144
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
.ewokssphinx-task .ewokssphinx-field-list {
|
|
2
|
+
padding-left: 0.5em;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.ewokssphinx-task .ewokssphinx-default {
|
|
6
|
+
background-color: transparent;
|
|
7
|
+
border: none;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.ewokssphinx-task .ewokssphinx-required {
|
|
11
|
+
color: var(--sd-color-danger, red)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.ewokssphinx-task .ewokssphinx-examples {
|
|
15
|
+
font-style: italic;
|
|
16
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from typing import Sequence
|
|
3
|
+
|
|
4
|
+
from docutils import nodes
|
|
5
|
+
from sphinx.util.docutils import SphinxDirective
|
|
6
|
+
|
|
7
|
+
from .type_utils import TaskDescription
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def task_nodes(
|
|
11
|
+
directive: SphinxDirective,
|
|
12
|
+
tasks: Sequence[dict[str, Any]],
|
|
13
|
+
) -> list[nodes.section]:
|
|
14
|
+
"""
|
|
15
|
+
Convert a list of JSON Ewoks task descriptions into Sphinx/Docutils
|
|
16
|
+
document node trees.
|
|
17
|
+
|
|
18
|
+
Returns a list of <section> nodes.
|
|
19
|
+
"""
|
|
20
|
+
return [_task_section(directive, task) for task in tasks]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _task_section(directive: SphinxDirective, task: dict[str, Any]) -> nodes.section:
|
|
24
|
+
_ = TaskDescription(**task)
|
|
25
|
+
|
|
26
|
+
section = nodes.section(
|
|
27
|
+
"",
|
|
28
|
+
ids=[task["task_identifier"]],
|
|
29
|
+
classes=["ewokssphinx-task"],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Title
|
|
33
|
+
section.append(nodes.title(text=task["task_name"]))
|
|
34
|
+
|
|
35
|
+
# Description
|
|
36
|
+
if task["description"]:
|
|
37
|
+
section.extend(_rst_to_nodes(directive, task["description"]))
|
|
38
|
+
|
|
39
|
+
# Identifier + task type
|
|
40
|
+
section.append(
|
|
41
|
+
nodes.field_list(
|
|
42
|
+
"",
|
|
43
|
+
_field_node("Identifier", nodes.literal(text=task["task_identifier"])),
|
|
44
|
+
_field_node("Task type", nodes.Text(task["task_type"])),
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Combine inputs + outputs in a container so that
|
|
49
|
+
# Sphinx does not attach the "simple" CSS class.
|
|
50
|
+
io_def = nodes.container("")
|
|
51
|
+
if task["inputs"]:
|
|
52
|
+
input_parameters = _parameter_nodes(
|
|
53
|
+
directive,
|
|
54
|
+
title="Inputs:",
|
|
55
|
+
css_classes=["field-odd"],
|
|
56
|
+
parameters=task["inputs"],
|
|
57
|
+
is_outputs=False,
|
|
58
|
+
)
|
|
59
|
+
io_def.append(input_parameters)
|
|
60
|
+
|
|
61
|
+
if task["outputs"]:
|
|
62
|
+
output_parameters = _parameter_nodes(
|
|
63
|
+
directive,
|
|
64
|
+
title="Outputs:",
|
|
65
|
+
css_classes=["field-even"],
|
|
66
|
+
parameters=task["outputs"],
|
|
67
|
+
is_outputs=True,
|
|
68
|
+
)
|
|
69
|
+
io_def.append(output_parameters)
|
|
70
|
+
|
|
71
|
+
section.append(
|
|
72
|
+
nodes.definition_list(
|
|
73
|
+
"",
|
|
74
|
+
io_def,
|
|
75
|
+
classes=["ewokssphinx-field-list"],
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return section
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _field_node(name: str, body: nodes.Element) -> nodes.field:
|
|
83
|
+
"""Construct a Sphinx field like: Identifier: my.module.Task"""
|
|
84
|
+
return nodes.field(
|
|
85
|
+
"",
|
|
86
|
+
nodes.field_name(text=name),
|
|
87
|
+
nodes.field_body("", body),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _term_node(name: str, is_required: bool) -> nodes.term:
|
|
92
|
+
"""
|
|
93
|
+
Create a term:
|
|
94
|
+
|
|
95
|
+
my_input
|
|
96
|
+
my_input*
|
|
97
|
+
"""
|
|
98
|
+
if not is_required:
|
|
99
|
+
return nodes.term(text=name)
|
|
100
|
+
|
|
101
|
+
# Required input -> add "*"
|
|
102
|
+
return nodes.term(
|
|
103
|
+
"",
|
|
104
|
+
name,
|
|
105
|
+
nodes.strong("", "*", classes=["ewokssphinx-required"]),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parameter_nodes(
|
|
110
|
+
directive: SphinxDirective,
|
|
111
|
+
title: str,
|
|
112
|
+
css_classes: list[str],
|
|
113
|
+
parameters: dict[str, str | list[str] | None],
|
|
114
|
+
is_outputs: bool,
|
|
115
|
+
) -> nodes.definition_list_item:
|
|
116
|
+
|
|
117
|
+
dl = nodes.definition_list()
|
|
118
|
+
for parameter in parameters:
|
|
119
|
+
dl.append(_parameter_node(directive, parameter, is_outputs))
|
|
120
|
+
|
|
121
|
+
return nodes.definition_list_item(
|
|
122
|
+
"",
|
|
123
|
+
nodes.term(text=title, classes=css_classes),
|
|
124
|
+
nodes.definition("", dl),
|
|
125
|
+
classes=["field-list"],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _parameter_node(
|
|
130
|
+
directive: SphinxDirective, parameter: dict[str, Any], is_output: bool
|
|
131
|
+
) -> nodes.definition_list_item:
|
|
132
|
+
"""
|
|
133
|
+
Full parameter description structure:
|
|
134
|
+
|
|
135
|
+
name* : annotation = default
|
|
136
|
+
Description...
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
'foo'
|
|
140
|
+
'bar'
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
# Build the term
|
|
144
|
+
#
|
|
145
|
+
# name* : annotation = default
|
|
146
|
+
#
|
|
147
|
+
is_required = parameter["required"] and not is_output
|
|
148
|
+
term = _term_node(parameter["name"], is_required)
|
|
149
|
+
|
|
150
|
+
if parameter["annotation"]:
|
|
151
|
+
term.extend([nodes.Text(" : "), nodes.literal(text=parameter["annotation"])])
|
|
152
|
+
|
|
153
|
+
has_default = (
|
|
154
|
+
parameter["has_default"] and not parameter["required"] and not is_output
|
|
155
|
+
)
|
|
156
|
+
if has_default:
|
|
157
|
+
term.append(
|
|
158
|
+
nodes.literal(
|
|
159
|
+
text=f"= {parameter['default']}",
|
|
160
|
+
classes=["ewokssphinx-default"],
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Description
|
|
165
|
+
definition = nodes.definition()
|
|
166
|
+
if parameter["description"]:
|
|
167
|
+
definition.extend(_rst_to_nodes(directive, parameter["description"]))
|
|
168
|
+
|
|
169
|
+
# Examples
|
|
170
|
+
examples = parameter.get("examples")
|
|
171
|
+
if examples:
|
|
172
|
+
definition.append(_examples_node(examples))
|
|
173
|
+
|
|
174
|
+
return nodes.definition_list_item("", term, definition)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _examples_node(examples: list[Any]) -> nodes.container:
|
|
178
|
+
bl = nodes.bullet_list()
|
|
179
|
+
for example in examples:
|
|
180
|
+
bl.append(nodes.list_item("", nodes.Text(example)))
|
|
181
|
+
|
|
182
|
+
return nodes.container(
|
|
183
|
+
"",
|
|
184
|
+
nodes.Text("Examples:"),
|
|
185
|
+
bl,
|
|
186
|
+
classes=["ewokssphinx-examples"],
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _rst_to_nodes(directive: SphinxDirective, text: str) -> list[nodes.Node]:
|
|
191
|
+
# TODO: directives like ".. note::" still appear as plain text.
|
|
192
|
+
return directive.parse_text_to_nodes(text)
|
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from sphinx.testing.util import SphinxTestApp
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture(scope="session")
|
|
8
|
+
def app(tmp_path_factory):
|
|
9
|
+
srcdir = tmp_path_factory.mktemp("app")
|
|
10
|
+
with open(srcdir / "conf.py", "w") as conf:
|
|
11
|
+
conf.write('extensions = ["ewokssphinx"]')
|
|
12
|
+
app = SphinxTestApp(
|
|
13
|
+
"html", srcdir=srcdir, docutils_conf="[readers]\ndocinfo_xform: no"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
return app
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(scope="session")
|
|
20
|
+
def app_with_cache(tmp_path_factory):
|
|
21
|
+
srcdir = tmp_path_factory.mktemp("app_with_cache")
|
|
22
|
+
json_cache_path = srcdir / "discovered_ewoks_tasks.json"
|
|
23
|
+
with open(srcdir / "conf.py", "w") as conf:
|
|
24
|
+
conf.write('extensions = ["ewokssphinx"]\n')
|
|
25
|
+
conf.write(f'ewokssphinx_task_cache_path = "{json_cache_path.as_posix()}"\n')
|
|
26
|
+
conf.write("ewokssphinx_ignore_discovery_error = True\n")
|
|
27
|
+
|
|
28
|
+
task = {
|
|
29
|
+
"task_name": "MyNonExistingTask",
|
|
30
|
+
"task_identifier": "ewoksnoexisting.tasks.MyNonExistingTask",
|
|
31
|
+
"task_type": "class",
|
|
32
|
+
"description": "My non-existing task documentation",
|
|
33
|
+
"inputs": [
|
|
34
|
+
{
|
|
35
|
+
"annotation": None,
|
|
36
|
+
"default": None,
|
|
37
|
+
"description": None,
|
|
38
|
+
"examples": [],
|
|
39
|
+
"has_default": False,
|
|
40
|
+
"name": "a",
|
|
41
|
+
"required": True,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"annotation": None,
|
|
45
|
+
"default": None,
|
|
46
|
+
"description": None,
|
|
47
|
+
"examples": [],
|
|
48
|
+
"has_default": False,
|
|
49
|
+
"name": "b",
|
|
50
|
+
"required": True,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"annotation": None,
|
|
54
|
+
"default": None,
|
|
55
|
+
"description": None,
|
|
56
|
+
"examples": [],
|
|
57
|
+
"has_default": False,
|
|
58
|
+
"name": "c",
|
|
59
|
+
"required": True,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"annotation": None,
|
|
63
|
+
"default": None,
|
|
64
|
+
"description": None,
|
|
65
|
+
"examples": [],
|
|
66
|
+
"has_default": False,
|
|
67
|
+
"name": "d",
|
|
68
|
+
"required": False,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"annotation": None,
|
|
72
|
+
"default": None,
|
|
73
|
+
"description": None,
|
|
74
|
+
"examples": [],
|
|
75
|
+
"has_default": False,
|
|
76
|
+
"name": "e",
|
|
77
|
+
"required": False,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
"outputs": [
|
|
81
|
+
{
|
|
82
|
+
"annotation": None,
|
|
83
|
+
"default": None,
|
|
84
|
+
"description": None,
|
|
85
|
+
"examples": [],
|
|
86
|
+
"has_default": False,
|
|
87
|
+
"name": "error",
|
|
88
|
+
"required": True,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"annotation": None,
|
|
92
|
+
"default": None,
|
|
93
|
+
"description": None,
|
|
94
|
+
"examples": [],
|
|
95
|
+
"has_default": False,
|
|
96
|
+
"name": "result",
|
|
97
|
+
"required": True,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
}
|
|
101
|
+
cache = {"ewoksnoexisting.tasks.MyNonExistingTask": {"class": [task]}}
|
|
102
|
+
with open(json_cache_path, "w") as fp:
|
|
103
|
+
json.dump(cache, fp)
|
|
104
|
+
|
|
105
|
+
app = SphinxTestApp(
|
|
106
|
+
"html", srcdir=srcdir, docutils_conf="[readers]\ndocinfo_xform: no"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return app
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from ewokscore import Task
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MyTask(
|
|
5
|
+
Task,
|
|
6
|
+
input_names=["a", "b", "c"],
|
|
7
|
+
optional_input_names=["d", "e"],
|
|
8
|
+
output_names=["result", "error"],
|
|
9
|
+
):
|
|
10
|
+
"""My task documentation"""
|
|
11
|
+
|
|
12
|
+
def run(self):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def my_other_task(f, g, h=None):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run(i, j=None, k=None):
|
|
21
|
+
"""Run task documentation"""
|
|
22
|
+
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _HiddenTask(
|
|
27
|
+
Task,
|
|
28
|
+
input_names=["one"],
|
|
29
|
+
optional_input_names=["two", "three"],
|
|
30
|
+
output_names=["result", "error"],
|
|
31
|
+
):
|
|
32
|
+
def run(self):
|
|
33
|
+
pass
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from ewokscore import Task
|
|
2
|
+
from ewokscore.model import BaseInputModel
|
|
3
|
+
from ewokscore.model import BaseOutputModel
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Inputs(BaseInputModel):
|
|
8
|
+
planet: str = "Earth"
|
|
9
|
+
latitude: int = Field(examples=[-90, 0, 90])
|
|
10
|
+
longitude: float = Field(
|
|
11
|
+
description="Longitude of the GPS point. **In degrees.**", examples=[-90, 0, 90]
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Outputs(BaseOutputModel):
|
|
16
|
+
location: str = Field(
|
|
17
|
+
..., description="Name of the closest city or location to the given coordinates"
|
|
18
|
+
)
|
|
19
|
+
error: Exception | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FindLocation(
|
|
23
|
+
Task,
|
|
24
|
+
input_model=Inputs,
|
|
25
|
+
output_model=Outputs,
|
|
26
|
+
):
|
|
27
|
+
"""Finds a location given the GPS coordinates"""
|
|
28
|
+
|
|
29
|
+
def run(self):
|
|
30
|
+
pass
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from sphinx.testing import restructuredtext
|
|
2
|
+
|
|
3
|
+
from .test_utils import assert_task_nodes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_cache(app_with_cache):
|
|
7
|
+
parsed_nodes = restructuredtext.parse(
|
|
8
|
+
app_with_cache,
|
|
9
|
+
""".. ewokstasks:: ewoksnoexisting.tasks.MyNonExistingTask
|
|
10
|
+
:task-type: class
|
|
11
|
+
""",
|
|
12
|
+
)
|
|
13
|
+
assert len(parsed_nodes) == 4
|
|
14
|
+
_assert_class_task_nodes(parsed_nodes)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _assert_class_task_nodes(nodes):
|
|
18
|
+
assert_task_nodes(
|
|
19
|
+
nodes,
|
|
20
|
+
identifier="ewoksnoexisting.tasks.MyNonExistingTask",
|
|
21
|
+
doc="""My non-existing task documentation""",
|
|
22
|
+
task_type="class",
|
|
23
|
+
required_inputs=["a", "b", "c"],
|
|
24
|
+
optional_inputs=["d", "e"],
|
|
25
|
+
outputs=["error", "result"],
|
|
26
|
+
)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from sphinx.testing import restructuredtext
|
|
3
|
+
|
|
4
|
+
from .test_utils import assert_task_nodes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_ewokstasks(app):
|
|
8
|
+
parsed_nodes = restructuredtext.parse(
|
|
9
|
+
app, ".. ewokstasks:: ewokssphinx.tests.dummy_tasks"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
assert len(parsed_nodes) == 4
|
|
13
|
+
_assert_class_task_nodes(parsed_nodes[0])
|
|
14
|
+
_assert_ppfmethod_nodes(parsed_nodes[1])
|
|
15
|
+
_assert_method_nodes(parsed_nodes[2:])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_ewokstasks_class(app):
|
|
19
|
+
parsed_nodes = restructuredtext.parse(
|
|
20
|
+
app,
|
|
21
|
+
""".. ewokstasks:: ewokssphinx.tests.dummy_tasks
|
|
22
|
+
:task-type: class
|
|
23
|
+
""",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
assert len(parsed_nodes) == 4
|
|
27
|
+
_assert_class_task_nodes(parsed_nodes)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_ewokstasks_method(app):
|
|
31
|
+
parsed_nodes = restructuredtext.parse(
|
|
32
|
+
app,
|
|
33
|
+
""".. ewokstasks:: ewokssphinx.tests.dummy_tasks
|
|
34
|
+
:task-type: method
|
|
35
|
+
""",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
assert len(parsed_nodes) == 2
|
|
39
|
+
_assert_method_nodes(parsed_nodes)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_ewokstasks_ppfmethod(app):
|
|
43
|
+
parsed_nodes = restructuredtext.parse(
|
|
44
|
+
app,
|
|
45
|
+
""".. ewokstasks:: ewokssphinx.tests.dummy_tasks
|
|
46
|
+
:task-type: ppfmethod
|
|
47
|
+
""",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
assert len(parsed_nodes) == 4
|
|
51
|
+
_assert_ppfmethod_nodes(parsed_nodes)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_ewokstasks_raised_import_error(app):
|
|
55
|
+
with pytest.raises(ImportError):
|
|
56
|
+
restructuredtext.parse(
|
|
57
|
+
app,
|
|
58
|
+
""".. ewokstasks:: not_existing
|
|
59
|
+
""",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_ewokstasks_ignore_import_error(app):
|
|
64
|
+
parsed_nodes = restructuredtext.parse(
|
|
65
|
+
app,
|
|
66
|
+
""".. ewokstasks:: not_existing
|
|
67
|
+
:ignore-import-error:
|
|
68
|
+
""",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
assert len(parsed_nodes) == 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _assert_class_task_nodes(nodes):
|
|
75
|
+
assert_task_nodes(
|
|
76
|
+
nodes,
|
|
77
|
+
identifier="ewokssphinx.tests.dummy_tasks.MyTask",
|
|
78
|
+
doc="""My task documentation""",
|
|
79
|
+
task_type="class",
|
|
80
|
+
required_inputs=["a", "b", "c"],
|
|
81
|
+
optional_inputs=["d", "e"],
|
|
82
|
+
outputs=["error", "result"],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _assert_method_nodes(nodes):
|
|
87
|
+
assert_task_nodes(
|
|
88
|
+
nodes[0],
|
|
89
|
+
identifier="ewokssphinx.tests.dummy_tasks.my_other_task",
|
|
90
|
+
doc=None,
|
|
91
|
+
task_type="method",
|
|
92
|
+
required_inputs=["f", "g"],
|
|
93
|
+
optional_inputs=["h"],
|
|
94
|
+
outputs=["return_value"],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
assert_task_nodes(
|
|
98
|
+
nodes[1],
|
|
99
|
+
identifier="ewokssphinx.tests.dummy_tasks.run",
|
|
100
|
+
doc="""Run task documentation""",
|
|
101
|
+
task_type="method",
|
|
102
|
+
required_inputs=["i"],
|
|
103
|
+
optional_inputs=["j", "k"],
|
|
104
|
+
outputs=["return_value"],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _assert_ppfmethod_nodes(nodes):
|
|
109
|
+
assert_task_nodes(
|
|
110
|
+
nodes,
|
|
111
|
+
identifier="ewokssphinx.tests.dummy_tasks.run",
|
|
112
|
+
doc="""Run task documentation""",
|
|
113
|
+
task_type="ppfmethod",
|
|
114
|
+
required_inputs=["i"],
|
|
115
|
+
optional_inputs=["j", "k"],
|
|
116
|
+
outputs=["return_value"],
|
|
117
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from docutils import nodes
|
|
2
|
+
from sphinx.testing import restructuredtext
|
|
3
|
+
|
|
4
|
+
from .test_utils import assert_node
|
|
5
|
+
from .test_utils import assert_task_preamble
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_ewokstasks_with_pydantic_input_model(app):
|
|
9
|
+
parsed_nodes = restructuredtext.parse(
|
|
10
|
+
app,
|
|
11
|
+
""".. ewokstasks:: ewokssphinx.tests.dummy_tasks_pydantic
|
|
12
|
+
:task-type: class
|
|
13
|
+
""",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
assert len(parsed_nodes) == 4
|
|
17
|
+
assert_task_preamble(
|
|
18
|
+
parsed_nodes,
|
|
19
|
+
"ewokssphinx.tests.dummy_tasks_pydantic.FindLocation",
|
|
20
|
+
"""Finds a location given the GPS coordinates""",
|
|
21
|
+
"class",
|
|
22
|
+
)
|
|
23
|
+
definition_list_node = parsed_nodes[-1]
|
|
24
|
+
container_node = definition_list_node[0]
|
|
25
|
+
|
|
26
|
+
input_list = container_node[0]
|
|
27
|
+
input_term, input_definition = input_list
|
|
28
|
+
assert_node(input_term, nodes.term, "Inputs:")
|
|
29
|
+
assert_node(input_definition, nodes.definition)
|
|
30
|
+
|
|
31
|
+
assert_node(input_definition[0][0][0], nodes.term, "planet : str= Earth")
|
|
32
|
+
assert_node(input_definition[0][1][0], nodes.term, "latitude* : int")
|
|
33
|
+
assert_node(input_definition[0][2][0], nodes.term, "longitude* : float")
|
|
34
|
+
|
|
35
|
+
longitude_definition = input_definition[0][2][1]
|
|
36
|
+
longitude_description, longitude_examples = longitude_definition
|
|
37
|
+
longitude_examples_title, longitude_examples = longitude_examples
|
|
38
|
+
assert_node(longitude_description, nodes.paragraph)
|
|
39
|
+
assert_node(longitude_description[0], nodes.Text, "Longitude of the GPS point. ")
|
|
40
|
+
assert_node(longitude_description[1], nodes.strong, "In degrees.")
|
|
41
|
+
assert_node(longitude_examples_title, nodes.Text, "Examples:")
|
|
42
|
+
for longitude_example, value in zip(longitude_examples, [-90, 0, 90]):
|
|
43
|
+
assert_node(longitude_example[0], nodes.Text, repr(value))
|
|
44
|
+
|
|
45
|
+
output_list = container_node[1]
|
|
46
|
+
output_term, output_definition = output_list
|
|
47
|
+
assert_node(output_term, nodes.term, "Outputs:")
|
|
48
|
+
assert_node(output_definition, nodes.definition)
|
|
49
|
+
|
|
50
|
+
assert_node(output_definition[0][0][0], nodes.term, "location : str")
|
|
51
|
+
assert_node(output_definition[0][1][0], nodes.term, "error : Exception | None")
|
|
52
|
+
|
|
53
|
+
location_definition = output_definition[0][0][1]
|
|
54
|
+
(location_definition,) = location_definition
|
|
55
|
+
assert_node(
|
|
56
|
+
location_definition[0],
|
|
57
|
+
nodes.Text,
|
|
58
|
+
"Name of the closest city or location to the given coordinates",
|
|
59
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from docutils import nodes
|
|
4
|
+
from docutils.nodes import Node
|
|
5
|
+
|
|
6
|
+
from ..ewoks_task_utils import _get_task_name
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def assert_node(node, cls: Type[Node], text: str | None = None):
|
|
10
|
+
assert isinstance(node, cls)
|
|
11
|
+
if text is not None:
|
|
12
|
+
assert node.astext() == text
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def assert_field_node(node, name: str, value: str):
|
|
16
|
+
assert isinstance(node, nodes.field)
|
|
17
|
+
assert_node(node[0], nodes.field_name, name)
|
|
18
|
+
assert_node(node[1], nodes.field_body, value)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def assert_simple_inputs(input_list, required_inputs, optional_inputs):
|
|
22
|
+
input_term, input_definition = input_list
|
|
23
|
+
assert_node(input_term, nodes.term, "Inputs:")
|
|
24
|
+
assert_node(input_definition, nodes.definition)
|
|
25
|
+
|
|
26
|
+
n_required = len(required_inputs)
|
|
27
|
+
for i in range(n_required):
|
|
28
|
+
assert_node(input_definition[0][i][0], nodes.term, f"{required_inputs[i]}*")
|
|
29
|
+
|
|
30
|
+
n_optional = len(optional_inputs)
|
|
31
|
+
for i in range(n_optional):
|
|
32
|
+
assert_node(
|
|
33
|
+
input_definition[0][n_required + i][0], nodes.term, optional_inputs[i]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def assert_simple_outputs(output_list, outputs):
|
|
38
|
+
output_term, output_definition = output_list
|
|
39
|
+
assert_node(output_term, nodes.term, "Outputs:")
|
|
40
|
+
assert_node(output_definition, nodes.definition)
|
|
41
|
+
for i, output_name in enumerate(outputs):
|
|
42
|
+
assert_node(output_definition[0][i][0], nodes.term, output_name)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def assert_task_nodes(
|
|
46
|
+
parsed_nodes, identifier, doc, task_type, required_inputs, optional_inputs, outputs
|
|
47
|
+
):
|
|
48
|
+
assert_task_preamble(parsed_nodes, identifier, doc, task_type)
|
|
49
|
+
definition_list_node = parsed_nodes[-1]
|
|
50
|
+
container_node = definition_list_node[0]
|
|
51
|
+
input_list = container_node[0]
|
|
52
|
+
assert_simple_inputs(
|
|
53
|
+
input_list,
|
|
54
|
+
required_inputs=required_inputs,
|
|
55
|
+
optional_inputs=optional_inputs,
|
|
56
|
+
)
|
|
57
|
+
output_list = container_node[1]
|
|
58
|
+
assert_simple_outputs(output_list, outputs=outputs)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def assert_task_preamble(parsed_nodes, identifier, doc, task_type):
|
|
62
|
+
name = _get_task_name(identifier, task_type)
|
|
63
|
+
assert_node(parsed_nodes[0], nodes.title, name)
|
|
64
|
+
if doc is not None:
|
|
65
|
+
assert_node(parsed_nodes[1], nodes.paragraph, doc)
|
|
66
|
+
field_list_nodes = parsed_nodes[2]
|
|
67
|
+
else:
|
|
68
|
+
field_list_nodes = parsed_nodes[1]
|
|
69
|
+
assert_node(field_list_nodes, nodes.field_list)
|
|
70
|
+
assert_field_node(field_list_nodes[0], name="Identifier", value=identifier)
|
|
71
|
+
assert_field_node(field_list_nodes[1], name="Task type", value=task_type)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TaskParameter(BaseModel, extra="forbid"):
|
|
5
|
+
name: str
|
|
6
|
+
annotation: str | None
|
|
7
|
+
required: bool
|
|
8
|
+
description: str | None
|
|
9
|
+
examples: list[str]
|
|
10
|
+
default: str | None
|
|
11
|
+
has_default: bool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TaskDescription(BaseModel, extra="forbid"):
|
|
15
|
+
task_name: str
|
|
16
|
+
task_identifier: str
|
|
17
|
+
task_type: str
|
|
18
|
+
description: str
|
|
19
|
+
inputs: list[TaskParameter]
|
|
20
|
+
outputs: list[TaskParameter]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ewokssphinx
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Ewoks Sphinx directives
|
|
5
|
+
Author-email: ESRF <dau-pydev@esrf.fr>
|
|
6
|
+
License: # MIT License
|
|
7
|
+
|
|
8
|
+
**Copyright (c) 2026 European Synchrotron Radiation Facility**
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
11
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
12
|
+
the Software without restriction, including without limitation the rights to
|
|
13
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
14
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
15
|
+
subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
22
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
23
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
24
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
25
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Homepage, https://github.com/ewoks-kit/ewokssphinx/
|
|
28
|
+
Project-URL: Documentation, https://github.com/ewoks-kit/ewokssphinx/blob/main/README.md
|
|
29
|
+
Project-URL: Repository, https://github.com/ewoks-kit/ewokssphinx/
|
|
30
|
+
Project-URL: Issues, https://github.com/ewoks-kit/ewokssphinx/issues
|
|
31
|
+
Project-URL: Changelog, https://github.com/ewoks-kit/ewokssphinx/blob/main/CHANGELOG.md
|
|
32
|
+
Keywords: ewoks,sphinx
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Framework :: Sphinx :: Extension
|
|
37
|
+
Requires-Python: >=3.10
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE.md
|
|
40
|
+
Requires-Dist: sphinx
|
|
41
|
+
Requires-Dist: ewokscore>=3.1.0
|
|
42
|
+
Requires-Dist: pydantic
|
|
43
|
+
Provides-Extra: test
|
|
44
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: ewokssphinx[test]; extra == "dev"
|
|
47
|
+
Requires-Dist: ruff; extra == "dev"
|
|
48
|
+
Provides-Extra: doc
|
|
49
|
+
Requires-Dist: ewokssphinx[test]; extra == "doc"
|
|
50
|
+
Requires-Dist: myst-parser; extra == "doc"
|
|
51
|
+
Requires-Dist: pydata_sphinx_theme; extra == "doc"
|
|
52
|
+
Requires-Dist: sphinx-design; extra == "doc"
|
|
53
|
+
Dynamic: license-file
|
|
54
|
+
|
|
55
|
+
# ewokssphinx
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
[](https://github.com/ewoks-kit/ewokssphinx/actions/workflows/test.yml)
|
|
59
|
+
[](https://github.com/psf/black)
|
|
60
|
+
[](https://github.com/ewoks-kit/ewokssphinx/blob/main/LICENSE.md)
|
|
61
|
+
[](https://codecov.io/gh/ewoks-kit/ewokssphinx)
|
|
62
|
+
[](https://ewokssphinx.readthedocs.io/en/latest/?badge=latest)
|
|
63
|
+
[](https://pypi.org/project/ewokssphinx/)
|
|
64
|
+
|
|
65
|
+
A set of Sphinx directives for Ewoks
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install ewokssphinx
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then, add `ewokssphinx` to the list of `extensions` in the Sphinx configuration file:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# conf.py
|
|
77
|
+
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
extensions = [
|
|
81
|
+
...,
|
|
82
|
+
"ewokssphinx"
|
|
83
|
+
]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Documentation
|
|
87
|
+
|
|
88
|
+
[Full documentation](https://ewokssphinx.readthedocs.io/en/latest) with [examples](https://ewokssphinx.readthedocs.io/en/latest/examples.html)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
ewokssphinx/__init__.py,sha256=PBgd9wyQEh2NyHaB9fD9lyYH7Nlk_0pPfMqOPjimayU,1290
|
|
2
|
+
ewokssphinx/ewoks_task_directive.py,sha256=Fs1nAvCbgeC7N1LBCycSzBYhlxa9swDlf9M4R2W8Mxg,1330
|
|
3
|
+
ewokssphinx/ewoks_task_utils.py,sha256=xyKDU-LicWt-xKEw4W52i_BTiFk5k80UdfYAIcYwfcs,4656
|
|
4
|
+
ewokssphinx/ewokssphinx.css,sha256=_K6rcN2skZOrG535NmmbB6Mgo9KZmR_vn11il-Qj9Rc,321
|
|
5
|
+
ewokssphinx/sphinx_task_utils.py,sha256=_yCrxP-8gSXYcLUB4gxW9nRfLZnJVhTpA5t0bWQ356k,4948
|
|
6
|
+
ewokssphinx/type_utils.py,sha256=tZ5mjikkaf9SS_epEO1r8-hVu2jrMMNHKk1ri1pcSBw,440
|
|
7
|
+
ewokssphinx/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
ewokssphinx/tests/conftest.py,sha256=oFJKQ5X0WYsPbrPUkLA4zzs-urlDblF_7MG4BeqGK-c,3287
|
|
9
|
+
ewokssphinx/tests/dummy_tasks.py,sha256=AzOcfyVJXyiHiv9oRg6dqD8iHftkUXaK67ty3cID4SY,518
|
|
10
|
+
ewokssphinx/tests/dummy_tasks_pydantic.py,sha256=du6r2zpdApNr36IbrRQPyYUqqsyvkDj-sfOa4fVaZTM,728
|
|
11
|
+
ewokssphinx/tests/test_task_cache.py,sha256=pIGra8-BxBusjpmtupEZrLnfZgCt5GjRIM5tK0_YVlo,723
|
|
12
|
+
ewokssphinx/tests/test_task_directive.py,sha256=V06dfDW-1Dx0Jw5dIXn2SBmPtyP9xzdzKWJUoSQQvuM,2884
|
|
13
|
+
ewokssphinx/tests/test_task_directive_pydantic.py,sha256=Jie1yE0Nhdo2kLRzsBpEmyIzYGbyokh1MakvVTerxMM,2328
|
|
14
|
+
ewokssphinx/tests/test_utils.py,sha256=HzU5vr2ENZ5_2TJA8-eJ5yeLb-PNJ6avFdKo_fvyruI,2469
|
|
15
|
+
ewokssphinx-2.1.0.dist-info/licenses/LICENSE.md,sha256=jhnnemwVcGW_Dc_FbaaD12x2Vz9mIqjX_MtyKrjw3gQ,1102
|
|
16
|
+
ewokssphinx-2.1.0.dist-info/METADATA,sha256=aLlPLLMUYuSY5-vxARrSy7EDXQttd7-PVdgd0m08IJg,3777
|
|
17
|
+
ewokssphinx-2.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
18
|
+
ewokssphinx-2.1.0.dist-info/top_level.txt,sha256=uj9wRIAtZZM_KtQWHn9rhn5tfZraEGwqMuNeDeV9AWY,12
|
|
19
|
+
ewokssphinx-2.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
**Copyright (c) 2026 European Synchrotron Radiation Facility**
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ewokssphinx
|