From fa616a279c195c94fdb1d8a6e250423ef30fdc60 Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Tue, 20 Jun 2023 09:08:58 +0200 Subject: [PATCH 01/36] moved functions and hardcoded inner pydantic models --- fractal_tasks_core/dev/lib_args_schemas.py | 51 ++----------------- fractal_tasks_core/dev/lib_descriptions.py | 58 ++++++++++++++++++++++ 2 files changed, 62 insertions(+), 47 deletions(-) create mode 100644 fractal_tasks_core/dev/lib_descriptions.py diff --git a/fractal_tasks_core/dev/lib_args_schemas.py b/fractal_tasks_core/dev/lib_args_schemas.py index 1b0f0b0e8..27a93b123 100644 --- a/fractal_tasks_core/dev/lib_args_schemas.py +++ b/fractal_tasks_core/dev/lib_args_schemas.py @@ -13,18 +13,18 @@ Helper functions to handle JSON schemas for task arguments. """ -import ast -from pathlib import Path from typing import Any -from docstring_parser import parse from pydantic.decorator import ALT_V_ARGS from pydantic.decorator import ALT_V_KWARGS from pydantic.decorator import V_DUPLICATE_KWARGS from pydantic.decorator import V_POSITIONAL_ONLY_NAME from pydantic.decorator import ValidatedFunction -import fractal_tasks_core +from fractal_tasks_core.dev.lib_descriptions import _get_args_descriptions +from fractal_tasks_core.dev.lib_descriptions import ( + _include_args_descriptions_in_schema, +) from fractal_tasks_core.dev.lib_signature_constraints import _extract_function from fractal_tasks_core.dev.lib_signature_constraints import ( _validate_function_signature, @@ -78,49 +78,6 @@ def _remove_pydantic_internals(old_schema: _Schema) -> _Schema: return new_schema -def _get_args_descriptions(executable) -> dict[str, str]: - """ - Extract argument descriptions for a task function - """ - # Read docstring (via ast) - module_path = Path(fractal_tasks_core.__file__).parent / executable - module_name = module_path.with_suffix("").name - tree = ast.parse(module_path.read_text()) - function = next( - f - for f in ast.walk(tree) - if (isinstance(f, ast.FunctionDef) and f.name == module_name) - ) - docstring = ast.get_docstring(function) - # Parse docstring (via docstring_parser) and prepare output - parsed_docstring = parse(docstring) - descriptions = { - param.arg_name: param.description.replace("\n", " ") - for param in parsed_docstring.params - } - return descriptions - - -def _include_args_descriptions_in_schema(*, schema, descriptions): - """ - Merge the descriptions obtained via `_get_args_descriptions` into an - existing JSON Schema for task arguments - """ - new_schema = schema.copy() - new_properties = schema["properties"].copy() - for key, value in schema["properties"].items(): - if "description" in value: - raise ValueError("Property already has description") - else: - if key in descriptions: - value["description"] = descriptions[key] - else: - value["description"] = "Missing description" - new_properties[key] = value - new_schema["properties"] = new_properties - return new_schema - - def create_schema_for_single_task( executable: str, package: str = "fractal_tasks_core.tasks", diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py new file mode 100644 index 000000000..97dcd54a0 --- /dev/null +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -0,0 +1,58 @@ +import ast +from pathlib import Path + +from docstring_parser import parse + +import fractal_tasks_core + + +inner_pydantic_models = { + "OmeroChannel": "fractal_tasks_core.lib_channels.py", + "Window": "fractal_tasks_core.lib_channels.py", + "Channel": "fractal_tasks_core.tasks._input_models.py", + "NapariWorkflowsInput": "fractal_tasks_core.tasks._input_models.py", + "NapariWorkflowsOutput": "fractal_tasks_core.tasks._input_models.py", +} + + +def _get_args_descriptions(executable) -> dict[str, str]: + """ + Extract argument descriptions for a task function + """ + # Read docstring (via ast) + module_path = Path(fractal_tasks_core.__file__).parent / executable + module_name = module_path.with_suffix("").name + tree = ast.parse(module_path.read_text()) + function = next( + f + for f in ast.walk(tree) + if (isinstance(f, ast.FunctionDef) and f.name == module_name) + ) + docstring = ast.get_docstring(function) + # Parse docstring (via docstring_parser) and prepare output + parsed_docstring = parse(docstring) + descriptions = { + param.arg_name: param.description.replace("\n", " ") + for param in parsed_docstring.params + } + return descriptions + + +def _include_args_descriptions_in_schema(*, schema, descriptions): + """ + Merge the descriptions obtained via `_get_args_descriptions` into an + existing JSON Schema for task arguments + """ + new_schema = schema.copy() + new_properties = schema["properties"].copy() + for key, value in schema["properties"].items(): + if "description" in value: + raise ValueError("Property already has description") + else: + if key in descriptions: + value["description"] = descriptions[key] + else: + value["description"] = "Missing description" + new_properties[key] = value + new_schema["properties"] = new_properties + return new_schema From 102d63a3eb367232c024a6419442966948a1f343 Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Tue, 20 Jun 2023 10:52:34 +0200 Subject: [PATCH 02/36] extract docstrings and var descriptions --- fractal_tasks_core/dev/lib_descriptions.py | 51 ++++++++++++++++++---- tests/test_unit_lib_descriptions.py | 9 ++++ 2 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 tests/test_unit_lib_descriptions.py diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index 97dcd54a0..2a067d512 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -1,20 +1,54 @@ import ast from pathlib import Path -from docstring_parser import parse +from docstring_parser import parse as docparse import fractal_tasks_core - inner_pydantic_models = { - "OmeroChannel": "fractal_tasks_core.lib_channels.py", - "Window": "fractal_tasks_core.lib_channels.py", - "Channel": "fractal_tasks_core.tasks._input_models.py", - "NapariWorkflowsInput": "fractal_tasks_core.tasks._input_models.py", - "NapariWorkflowsOutput": "fractal_tasks_core.tasks._input_models.py", + "OmeroChannel": "lib_channels.py", + "Window": "lib_channels.py", + "Channel": "tasks/_input_models.py", + "NapariWorkflowsInput": "tasks/_input_models.py", + "NapariWorkflowsOutput": "tasks/_input_models.py", } +def _get_args_model_descriptions(): + + descriptions = {} + + for model, module in inner_pydantic_models.items(): + + module_path = Path(fractal_tasks_core.__file__).parent / module + tree = ast.parse(module_path.read_text()) + _class = next( + c + for c in ast.walk(tree) + if (isinstance(c, ast.ClassDef) and c.name == model) + ) + if not _class: + raise RuntimeError(f"Model {module_path}::{model} not found.") + else: + descriptions[model] = {} + + docstring = ast.get_docstring(_class) + if docstring: + descriptions[model]["docstring"] = docstring + + var_name: str = "" + for node in _class.body: + if isinstance(node, ast.AnnAssign): + descriptions[model][node.target.id] = None + var_name = node.target.id + else: + if isinstance(node, ast.Expr) and var_name: + descriptions[model][var_name] = node.value.s + var_name = "" + + return descriptions + + def _get_args_descriptions(executable) -> dict[str, str]: """ Extract argument descriptions for a task function @@ -28,9 +62,10 @@ def _get_args_descriptions(executable) -> dict[str, str]: for f in ast.walk(tree) if (isinstance(f, ast.FunctionDef) and f.name == module_name) ) + docstring = ast.get_docstring(function) # Parse docstring (via docstring_parser) and prepare output - parsed_docstring = parse(docstring) + parsed_docstring = docparse(docstring) descriptions = { param.arg_name: param.description.replace("\n", " ") for param in parsed_docstring.params diff --git a/tests/test_unit_lib_descriptions.py b/tests/test_unit_lib_descriptions.py new file mode 100644 index 000000000..7959528cd --- /dev/null +++ b/tests/test_unit_lib_descriptions.py @@ -0,0 +1,9 @@ +from devtools import debug + +from fractal_tasks_core.dev.lib_descriptions import ( + _get_args_model_descriptions, +) + + +def test_get_args_model_descriptions(): + debug(_get_args_model_descriptions()) From 074c36feaeb31122674c866489a69311426a781f Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Tue, 20 Jun 2023 10:56:09 +0200 Subject: [PATCH 03/36] docstring to __docstring__ --- fractal_tasks_core/dev/lib_descriptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index 2a067d512..f7e7ee78c 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -34,7 +34,7 @@ def _get_args_model_descriptions(): docstring = ast.get_docstring(_class) if docstring: - descriptions[model]["docstring"] = docstring + descriptions[model]["__docstring__"] = docstring var_name: str = "" for node in _class.body: From 1a8512b0b42582185223b91ab8da77e465889b85 Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Tue, 20 Jun 2023 11:07:22 +0200 Subject: [PATCH 04/36] moved function and add docs --- fractal_tasks_core/dev/lib_descriptions.py | 91 ++++++++++++---------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index f7e7ee78c..abcd49cf0 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -5,49 +5,6 @@ import fractal_tasks_core -inner_pydantic_models = { - "OmeroChannel": "lib_channels.py", - "Window": "lib_channels.py", - "Channel": "tasks/_input_models.py", - "NapariWorkflowsInput": "tasks/_input_models.py", - "NapariWorkflowsOutput": "tasks/_input_models.py", -} - - -def _get_args_model_descriptions(): - - descriptions = {} - - for model, module in inner_pydantic_models.items(): - - module_path = Path(fractal_tasks_core.__file__).parent / module - tree = ast.parse(module_path.read_text()) - _class = next( - c - for c in ast.walk(tree) - if (isinstance(c, ast.ClassDef) and c.name == model) - ) - if not _class: - raise RuntimeError(f"Model {module_path}::{model} not found.") - else: - descriptions[model] = {} - - docstring = ast.get_docstring(_class) - if docstring: - descriptions[model]["__docstring__"] = docstring - - var_name: str = "" - for node in _class.body: - if isinstance(node, ast.AnnAssign): - descriptions[model][node.target.id] = None - var_name = node.target.id - else: - if isinstance(node, ast.Expr) and var_name: - descriptions[model][var_name] = node.value.s - var_name = "" - - return descriptions - def _get_args_descriptions(executable) -> dict[str, str]: """ @@ -91,3 +48,51 @@ def _include_args_descriptions_in_schema(*, schema, descriptions): new_properties[key] = value new_schema["properties"] = new_properties return new_schema + + +INNER_PYDANTIC_MODELS = { + "OmeroChannel": "lib_channels.py", + "Window": "lib_channels.py", + "Channel": "tasks/_input_models.py", + "NapariWorkflowsInput": "tasks/_input_models.py", + "NapariWorkflowsOutput": "tasks/_input_models.py", +} + + +def _get_args_model_descriptions( + models: dict[str, str] = INNER_PYDANTIC_MODELS +): + """ + Extract attribut descriptions for Pydantic models + """ + descriptions = {} + + for model, module in models.items(): + # get the class + module_path = Path(fractal_tasks_core.__file__).parent / module + tree = ast.parse(module_path.read_text()) + _class = next( + c + for c in ast.walk(tree) + if (isinstance(c, ast.ClassDef) and c.name == model) + ) + if not _class: + raise RuntimeError(f"Model {module_path}::{model} not found.") + else: + descriptions[model] = {} + + # extract class docstring and attribute docstrings + docstring = ast.get_docstring(_class) + if docstring: + descriptions[model]["_class_docstring_"] = docstring + var_name: str = "" + for node in _class.body: + if isinstance(node, ast.AnnAssign): + descriptions[model][node.target.id] = None + var_name = node.target.id + else: + if isinstance(node, ast.Expr) and var_name: + descriptions[model][var_name] = node.value.s + var_name = "" + + return descriptions From d52a6af9e048cc864faee0ccf9dc75e5ae3b27fc Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Tue, 20 Jun 2023 11:09:23 +0200 Subject: [PATCH 05/36] change func name --- fractal_tasks_core/dev/lib_descriptions.py | 2 +- tests/test_unit_lib_descriptions.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index abcd49cf0..6e7666e1b 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -59,7 +59,7 @@ def _include_args_descriptions_in_schema(*, schema, descriptions): } -def _get_args_model_descriptions( +def _get_attributes_model_descriptions( models: dict[str, str] = INNER_PYDANTIC_MODELS ): """ diff --git a/tests/test_unit_lib_descriptions.py b/tests/test_unit_lib_descriptions.py index 7959528cd..fe2534a2d 100644 --- a/tests/test_unit_lib_descriptions.py +++ b/tests/test_unit_lib_descriptions.py @@ -1,9 +1,9 @@ from devtools import debug from fractal_tasks_core.dev.lib_descriptions import ( - _get_args_model_descriptions, + _get_attributes_model_descriptions, ) def test_get_args_model_descriptions(): - debug(_get_args_model_descriptions()) + debug(_get_attributes_model_descriptions()) From 818f17bcd6a537389dfc316ce2ad09760d859141 Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Tue, 20 Jun 2023 11:32:40 +0200 Subject: [PATCH 06/36] implement _include_attributs_descriptions_in_schema --- fractal_tasks_core/dev/lib_descriptions.py | 49 ++++++++++++++++++---- tests/test_unit_lib_descriptions.py | 4 +- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index 6e7666e1b..87cac4f9d 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -8,7 +8,7 @@ def _get_args_descriptions(executable) -> dict[str, str]: """ - Extract argument descriptions for a task function + Extract argument descriptions for a task function. """ # Read docstring (via ast) module_path = Path(fractal_tasks_core.__file__).parent / executable @@ -33,7 +33,7 @@ def _get_args_descriptions(executable) -> dict[str, str]: def _include_args_descriptions_in_schema(*, schema, descriptions): """ Merge the descriptions obtained via `_get_args_descriptions` into an - existing JSON Schema for task arguments + existing JSON Schema for task arguments. """ new_schema = schema.copy() new_properties = schema["properties"].copy() @@ -59,11 +59,22 @@ def _include_args_descriptions_in_schema(*, schema, descriptions): } -def _get_attributes_model_descriptions( +def _get_attributes_models_descriptions( models: dict[str, str] = INNER_PYDANTIC_MODELS -): +) -> dict[str, dict[str, str]]: """ Extract attribut descriptions for Pydantic models + + Returns: + descriptions == { + ... , + 'Channel': { + '_class_docstring_': '...', + 'wavelength_id': None, + 'label': None, + }, + ... , + } """ descriptions = {} @@ -77,14 +88,11 @@ def _get_attributes_model_descriptions( if (isinstance(c, ast.ClassDef) and c.name == model) ) if not _class: - raise RuntimeError(f"Model {module_path}::{model} not found.") + raise ValueError(f"Model {module_path}::{model} not found.") else: descriptions[model] = {} - # extract class docstring and attribute docstrings - docstring = ast.get_docstring(_class) - if docstring: - descriptions[model]["_class_docstring_"] = docstring + # extract attribute docstrings var_name: str = "" for node in _class.body: if isinstance(node, ast.AnnAssign): @@ -96,3 +104,26 @@ def _get_attributes_model_descriptions( var_name = "" return descriptions + + +def _include_attributs_descriptions_in_schema(*, schema, descriptions): + """ + Merge the descriptions obtained via `_get_attributes_models_descriptions` + into an existing JSON Schema for task arguments. + """ + new_schema = schema.copy() + new_definitions = schema["definitions"].copy() + + for key, value in schema["definitions"].items(): + if key in descriptions: + for attribute in descriptions[key]: + if attribute in value["properties"]: + if "description" in value["properties"]: + raise ValueError("Attribute already has description") + else: + new_definitions[key]["properties"][ + "description" + ] = descriptions[key][attribute] + + new_schema["definitions"] = new_definitions + return new_schema diff --git a/tests/test_unit_lib_descriptions.py b/tests/test_unit_lib_descriptions.py index fe2534a2d..0b7275509 100644 --- a/tests/test_unit_lib_descriptions.py +++ b/tests/test_unit_lib_descriptions.py @@ -1,9 +1,9 @@ from devtools import debug from fractal_tasks_core.dev.lib_descriptions import ( - _get_attributes_model_descriptions, + _get_attributes_models_descriptions, ) def test_get_args_model_descriptions(): - debug(_get_attributes_model_descriptions()) + debug(_get_attributes_models_descriptions()) From 5b28e543e6f4099acea3728082c1bd5e9cec6209 Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Wed, 21 Jun 2023 09:18:35 +0200 Subject: [PATCH 07/36] BROKEN stash --- fractal_tasks_core/dev/lib_descriptions.py | 3 ++- tests/test_unit_lib_descriptions.py | 21 +++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index 87cac4f9d..fff3bac98 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -1,4 +1,5 @@ import ast +import logging from pathlib import Path from docstring_parser import parse as docparse @@ -119,7 +120,7 @@ def _include_attributs_descriptions_in_schema(*, schema, descriptions): for attribute in descriptions[key]: if attribute in value["properties"]: if "description" in value["properties"]: - raise ValueError("Attribute already has description") + logging.warning("Attribute already has description") else: new_definitions[key]["properties"][ "description" diff --git a/tests/test_unit_lib_descriptions.py b/tests/test_unit_lib_descriptions.py index 0b7275509..e30ee27fd 100644 --- a/tests/test_unit_lib_descriptions.py +++ b/tests/test_unit_lib_descriptions.py @@ -1,9 +1,26 @@ -from devtools import debug +import json from fractal_tasks_core.dev.lib_descriptions import ( _get_attributes_models_descriptions, ) +from fractal_tasks_core.dev.lib_descriptions import ( + _include_attributs_descriptions_in_schema, +) def test_get_args_model_descriptions(): - debug(_get_attributes_models_descriptions()) + + descriptions = _get_attributes_models_descriptions() + + with open("fractal_tasks_core/__FRACTAL_MANIFEST__.json", "r") as f: + manifest = json.load(f) + + schemas = [task["args_schema"] for task in manifest["task_list"]] + + for schema in schemas: + new_schema = _include_attributs_descriptions_in_schema( + schema=schema, descriptions=descriptions + ) + for _, definition in new_schema["definitions"].items(): + for prop in definition["properties"]: + assert "description" in prop From 75b18f466e8efa8c1b6fccf36a100601a368d297 Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Wed, 21 Jun 2023 14:01:26 +0200 Subject: [PATCH 08/36] fix _include_attributs_descriptions_in_schema and test --- fractal_tasks_core/dev/lib_descriptions.py | 41 ++++++++++++---------- fractal_tasks_core/lib_input_models.py | 8 +++++ tests/test_unit_lib_descriptions.py | 13 ++++--- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index fff3bac98..3fc20042e 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -54,9 +54,9 @@ def _include_args_descriptions_in_schema(*, schema, descriptions): INNER_PYDANTIC_MODELS = { "OmeroChannel": "lib_channels.py", "Window": "lib_channels.py", - "Channel": "tasks/_input_models.py", - "NapariWorkflowsInput": "tasks/_input_models.py", - "NapariWorkflowsOutput": "tasks/_input_models.py", + "Channel": "lib_input_models.py", + "NapariWorkflowsInput": "lib_input_models.py", + "NapariWorkflowsOutput": "lib_input_models.py", } @@ -70,9 +70,8 @@ def _get_attributes_models_descriptions( descriptions == { ... , 'Channel': { - '_class_docstring_': '...', - 'wavelength_id': None, - 'label': None, + 'wavelength_id': TBD, + 'label': TBD, }, ... , } @@ -97,7 +96,7 @@ def _get_attributes_models_descriptions( var_name: str = "" for node in _class.body: if isinstance(node, ast.AnnAssign): - descriptions[model][node.target.id] = None + descriptions[model][node.target.id] = "Missing description" var_name = node.target.id else: if isinstance(node, ast.Expr) and var_name: @@ -113,18 +112,22 @@ def _include_attributs_descriptions_in_schema(*, schema, descriptions): into an existing JSON Schema for task arguments. """ new_schema = schema.copy() - new_definitions = schema["definitions"].copy() - - for key, value in schema["definitions"].items(): - if key in descriptions: - for attribute in descriptions[key]: - if attribute in value["properties"]: - if "description" in value["properties"]: - logging.warning("Attribute already has description") - else: - new_definitions[key]["properties"][ - "description" - ] = descriptions[key][attribute] + if "definitions" not in schema: + return new_schema + else: + new_definitions = schema["definitions"].copy() + + for name, definition in schema["definitions"].items(): + if name in descriptions.keys(): + for prop in definition["properties"]: + if "description" in new_definitions[name]["properties"][prop]: + logging.warning( + f"Property {name}.{prop} already has description" + ) + else: + new_definitions[name]["properties"][prop][ + "description" + ] = descriptions[name][prop] new_schema["definitions"] = new_definitions return new_schema diff --git a/fractal_tasks_core/lib_input_models.py b/fractal_tasks_core/lib_input_models.py index f6f70de57..3ff278e28 100644 --- a/fractal_tasks_core/lib_input_models.py +++ b/fractal_tasks_core/lib_input_models.py @@ -26,7 +26,9 @@ class Channel(BaseModel): """ wavelength_id: Optional[str] = None + """TBD""" label: Optional[str] = None + """TBD""" @validator("label", always=True) def mutually_exclusive_channel_attributes(cls, v, values): @@ -53,8 +55,11 @@ class NapariWorkflowsInput(BaseModel): """ type: Literal["image", "label"] + """TBD""" label_name: Optional[str] + """TBD""" channel: Optional[Channel] + """TBD""" @validator("label_name", always=True) def label_name_is_present(cls, v, values): @@ -79,8 +84,11 @@ class NapariWorkflowsOutput(BaseModel): """ type: Literal["label", "dataframe"] + """TBD""" label_name: Optional[str] = None + """TBD""" table_name: Optional[str] = None + """TBD""" @validator("label_name", always=True) def label_name_only_for_label_type(cls, v, values): diff --git a/tests/test_unit_lib_descriptions.py b/tests/test_unit_lib_descriptions.py index e30ee27fd..8bfba4b56 100644 --- a/tests/test_unit_lib_descriptions.py +++ b/tests/test_unit_lib_descriptions.py @@ -1,5 +1,7 @@ import json +from devtools import debug + from fractal_tasks_core.dev.lib_descriptions import ( _get_attributes_models_descriptions, ) @@ -9,18 +11,19 @@ def test_get_args_model_descriptions(): - descriptions = _get_attributes_models_descriptions() + debug(descriptions) with open("fractal_tasks_core/__FRACTAL_MANIFEST__.json", "r") as f: manifest = json.load(f) schemas = [task["args_schema"] for task in manifest["task_list"]] - for schema in schemas: + for i, schema in enumerate(schemas): new_schema = _include_attributs_descriptions_in_schema( schema=schema, descriptions=descriptions ) - for _, definition in new_schema["definitions"].items(): - for prop in definition["properties"]: - assert "description" in prop + if "definitions" in schema: + for _, definition in new_schema["definitions"].items(): + for prop in definition["properties"]: + assert "description" in definition["properties"][prop] From b9e098fe1d556c0e78cf3e4de5759e706b9b4b8f Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Wed, 21 Jun 2023 14:13:58 +0200 Subject: [PATCH 09/36] change log warning to raise error --- fractal_tasks_core/dev/lib_descriptions.py | 19 +++++++++---------- tests/test_unit_lib_descriptions.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index 3fc20042e..f65cea615 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -1,5 +1,4 @@ import ast -import logging from pathlib import Path from docstring_parser import parse as docparse @@ -82,15 +81,15 @@ def _get_attributes_models_descriptions( # get the class module_path = Path(fractal_tasks_core.__file__).parent / module tree = ast.parse(module_path.read_text()) - _class = next( - c - for c in ast.walk(tree) - if (isinstance(c, ast.ClassDef) and c.name == model) - ) - if not _class: - raise ValueError(f"Model {module_path}::{model} not found.") - else: + try: + _class = next( + c + for c in ast.walk(tree) + if (isinstance(c, ast.ClassDef) and c.name == model) + ) descriptions[model] = {} + except StopIteration: + raise ValueError(f"Model {module_path}::{model} not found.") # extract attribute docstrings var_name: str = "" @@ -122,7 +121,7 @@ def _include_attributs_descriptions_in_schema(*, schema, descriptions): if name in descriptions.keys(): for prop in definition["properties"]: if "description" in new_definitions[name]["properties"][prop]: - logging.warning( + raise ValueError( f"Property {name}.{prop} already has description" ) else: diff --git a/tests/test_unit_lib_descriptions.py b/tests/test_unit_lib_descriptions.py index 8bfba4b56..067824459 100644 --- a/tests/test_unit_lib_descriptions.py +++ b/tests/test_unit_lib_descriptions.py @@ -1,5 +1,6 @@ import json +import pytest from devtools import debug from fractal_tasks_core.dev.lib_descriptions import ( @@ -11,6 +12,10 @@ def test_get_args_model_descriptions(): + + with pytest.raises(ValueError): + _get_attributes_models_descriptions(models={"Foo": "__init__.py"}) + descriptions = _get_attributes_models_descriptions() debug(descriptions) @@ -27,3 +32,8 @@ def test_get_args_model_descriptions(): for _, definition in new_schema["definitions"].items(): for prop in definition["properties"]: assert "description" in definition["properties"][prop] + + with pytest.raises(ValueError): + _include_attributs_descriptions_in_schema( + schema=new_schema, descriptions=descriptions + ) From f3a601e0f7551077f640901062f5f151fb3879eb Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Wed, 21 Jun 2023 14:45:42 +0200 Subject: [PATCH 10/36] Update autodoc_member_orderautodoc_member_order option in docs --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 9b3fc042f..7f1d043ab 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -29,6 +29,7 @@ "private-members": True, "show-inheritance": True, "autosummary-no-nesting": True, + "autodoc_member_order": "bysource" # valid options: bysource, groupwise, alphabetical } autodata_content = "both" source_suffix = ".rst" From cc0400f27e627b0e535f107f007c380a16cc8551 Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Wed, 21 Jun 2023 14:52:53 +0200 Subject: [PATCH 11/36] Fix precommit --- docs/source/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7f1d043ab..63f7e1a2a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -29,7 +29,8 @@ "private-members": True, "show-inheritance": True, "autosummary-no-nesting": True, - "autodoc_member_order": "bysource" # valid options: bysource, groupwise, alphabetical + # valid options: bysource, groupwise, alphabetical + "autodoc_member_order": "bysource", } autodata_content = "both" source_suffix = ".rst" From b11fc4ed356220e5abaeeca9a9dc2574f124cd5d Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Wed, 21 Jun 2023 16:14:10 +0200 Subject: [PATCH 12/36] class attribute docstrings back to """...""" --- fractal_tasks_core/lib_channels.py | 11 +++++++++++ fractal_tasks_core/lib_input_models.py | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/fractal_tasks_core/lib_channels.py b/fractal_tasks_core/lib_channels.py index a7053ed4d..1899bc9ca 100644 --- a/fractal_tasks_core/lib_channels.py +++ b/fractal_tasks_core/lib_channels.py @@ -45,10 +45,13 @@ class Window(BaseModel): min: Optional[int] """TBD""" + max: Optional[int] """TBD""" + start: int """TBD""" + end: int """TBD""" @@ -68,22 +71,30 @@ class OmeroChannel(BaseModel): """ # Custom + wavelength_id: str """TBD""" + index: Optional[int] """TBD""" # From OME-NGFF v0.4 transitional metadata + window: Optional[Window] """TBD""" + color: Optional[str] """TBD""" + label: Optional[str] """TBD""" + active: bool = True """TBD""" + coefficient: int = 1 """TBD""" + inverted: bool = False """TBD""" diff --git a/fractal_tasks_core/lib_input_models.py b/fractal_tasks_core/lib_input_models.py index 3ff278e28..4c41e9b82 100644 --- a/fractal_tasks_core/lib_input_models.py +++ b/fractal_tasks_core/lib_input_models.py @@ -27,6 +27,7 @@ class Channel(BaseModel): wavelength_id: Optional[str] = None """TBD""" + label: Optional[str] = None """TBD""" @@ -56,8 +57,10 @@ class NapariWorkflowsInput(BaseModel): type: Literal["image", "label"] """TBD""" + label_name: Optional[str] """TBD""" + channel: Optional[Channel] """TBD""" @@ -85,8 +88,10 @@ class NapariWorkflowsOutput(BaseModel): type: Literal["label", "dataframe"] """TBD""" + label_name: Optional[str] = None """TBD""" + table_name: Optional[str] = None """TBD""" From e35555fc1d50a3dbe208a93b6bd164dfe6a593ba Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Wed, 21 Jun 2023 16:17:53 +0200 Subject: [PATCH 13/36] Simplify docstring to fix docs build --- fractal_tasks_core/dev/lib_descriptions.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index f65cea615..3f0ed5c76 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -64,16 +64,6 @@ def _get_attributes_models_descriptions( ) -> dict[str, dict[str, str]]: """ Extract attribut descriptions for Pydantic models - - Returns: - descriptions == { - ... , - 'Channel': { - 'wavelength_id': TBD, - 'label': TBD, - }, - ... , - } """ descriptions = {} From 36b072abefcf0521161af6f1b41d3d6468f1009e Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Wed, 21 Jun 2023 16:33:58 +0200 Subject: [PATCH 14/36] STASH --- fractal_tasks_core/dev/lib_descriptions.py | 45 +++++++++++++++++----- tests/test_unit_lib_descriptions.py | 9 +++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index f65cea615..d72862620 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -1,10 +1,45 @@ import ast +from importlib import import_module from pathlib import Path from docstring_parser import parse as docparse import fractal_tasks_core +# import inspect +# from inspect import signature +# from typing import Callable + + +""" +pkg_name = fractal_task_core +relative_module_path = lib_channels.py +object_name = {class_name} / {function_name} + + +_get_function_args_descriptions: dict[str, str] +_get_class_attrs_descriptions: dict[str, str] + +l'import deve essere fatto tipo: + module = import_module(f"{package}.{module_name}") + task_function = getattr(module, module_name) + +Per una funzione deve ritornare quello che già abbiamo +Per una classe dobbiamo fare quello che facciamo ma per una sola classe +""" + + +def _get_function_args_descriptions( + package_name: str, module_relative_path: str, function_name: str +) -> dict[str, str]: + + if not module_relative_path.endswith(".py"): + raise ValueError(f"Module {module_relative_path} must end with '.py'") + + module = import_module(f"{package_name}.{module_relative_path}") + x = getattr(module, function_name) + return x + def _get_args_descriptions(executable) -> dict[str, str]: """ @@ -64,16 +99,6 @@ def _get_attributes_models_descriptions( ) -> dict[str, dict[str, str]]: """ Extract attribut descriptions for Pydantic models - - Returns: - descriptions == { - ... , - 'Channel': { - 'wavelength_id': TBD, - 'label': TBD, - }, - ... , - } """ descriptions = {} diff --git a/tests/test_unit_lib_descriptions.py b/tests/test_unit_lib_descriptions.py index 067824459..4dd989bdf 100644 --- a/tests/test_unit_lib_descriptions.py +++ b/tests/test_unit_lib_descriptions.py @@ -6,6 +6,9 @@ from fractal_tasks_core.dev.lib_descriptions import ( _get_attributes_models_descriptions, ) +from fractal_tasks_core.dev.lib_descriptions import ( + _get_function_args_descriptions, +) from fractal_tasks_core.dev.lib_descriptions import ( _include_attributs_descriptions_in_schema, ) @@ -13,6 +16,12 @@ def test_get_args_model_descriptions(): + x = _get_function_args_descriptions( + "fractal_tasks_core", "lib_input_models.py", "Channel" + ) + debug(x, dir(x)) + # assert False + with pytest.raises(ValueError): _get_attributes_models_descriptions(models={"Foo": "__init__.py"}) From 10f21571714f6bb2a9eb2cd161fb131bb5fe5956 Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Wed, 21 Jun 2023 16:42:30 +0200 Subject: [PATCH 15/36] Improve docstrings (ref #421) --- fractal_tasks_core/lib_channels.py | 32 +++++++++++++------------- fractal_tasks_core/lib_input_models.py | 30 ++++++++++++++++-------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/fractal_tasks_core/lib_channels.py b/fractal_tasks_core/lib_channels.py index 1899bc9ca..2db8388fb 100644 --- a/fractal_tasks_core/lib_channels.py +++ b/fractal_tasks_core/lib_channels.py @@ -40,20 +40,20 @@ class Window(BaseModel): Main difference from the specs: 1. ``min`` and ``max`` are optional, since we have custom logic to set - their values. + their values. """ min: Optional[int] - """TBD""" + """If not provided, it will be set to ``0``.""" max: Optional[int] - """TBD""" + """If not provided, it will be set according to bit-depth.""" start: int - """TBD""" + """Start value of the visualization window.""" end: int - """TBD""" + """End value of the visualization window.""" class OmeroChannel(BaseModel): @@ -61,7 +61,7 @@ class OmeroChannel(BaseModel): Custom class for Omero channels, related to OME-NGFF v0.4. Differences from OME-NGFF v0.4 specs - (https://ngff.openmicroscopy.org/0.4/#omero-md) + (https://ngff.openmicroscopy.org/0.4/#omero-md): 1. Additional attributes ``wavelength_id`` and ``index``. 2. We make ``color`` an optional attribute, since we have custom @@ -73,30 +73,30 @@ class OmeroChannel(BaseModel): # Custom wavelength_id: str - """TBD""" + """Custom attribute (e.g. ``A01_C01``).""" index: Optional[int] - """TBD""" + """For internal use only.""" # From OME-NGFF v0.4 transitional metadata + label: Optional[str] + """Channel label.""" + window: Optional[Window] - """TBD""" + """A ``Window`` object to display this channel in napari.""" color: Optional[str] - """TBD""" - - label: Optional[str] - """TBD""" + """A colormap to display the channel in napari (e.g. ``00FFFF``).""" active: bool = True - """TBD""" + """Omero-channel attribute.""" coefficient: int = 1 - """TBD""" + """Omero-channel attribute.""" inverted: bool = False - """TBD""" + """Omero-channel attribute.""" class ChannelNotFoundError(ValueError): diff --git a/fractal_tasks_core/lib_input_models.py b/fractal_tasks_core/lib_input_models.py index 4c41e9b82..0fcefa928 100644 --- a/fractal_tasks_core/lib_input_models.py +++ b/fractal_tasks_core/lib_input_models.py @@ -26,15 +26,15 @@ class Channel(BaseModel): """ wavelength_id: Optional[str] = None - """TBD""" + """Unique ID for the channel wavelength, e.g. ``A01_C01``.""" label: Optional[str] = None - """TBD""" + """Channel label.""" @validator("label", always=True) def mutually_exclusive_channel_attributes(cls, v, values): """ - Attributes ``label`` and ``wavelength_id`` are mutually exclusive. + Check that either ``label`` or ``wavelength_id`` is set. """ wavelength_id = values.get("wavelength_id") label = v @@ -56,16 +56,19 @@ class NapariWorkflowsInput(BaseModel): """ type: Literal["image", "label"] - """TBD""" + """Input type (supported: ``image`` or ``label``).""" label_name: Optional[str] - """TBD""" + """Label name (for label inputs only).""" channel: Optional[Channel] - """TBD""" + """Channel object (for image inputs only).""" @validator("label_name", always=True) def label_name_is_present(cls, v, values): + """ + Check that label inputs have ``label_name`` set. + """ _type = values.get("type") if _type == "label" and not v: raise ValueError( @@ -75,6 +78,9 @@ def label_name_is_present(cls, v, values): @validator("channel", always=True) def channel_is_present(cls, v, values): + """ + Check that image inputs have ``channel`` set. + """ _type = values.get("type") if _type == "image" and not v: raise ValueError(f"Input item has type={_type} but channel={v}.") @@ -87,16 +93,19 @@ class NapariWorkflowsOutput(BaseModel): """ type: Literal["label", "dataframe"] - """TBD""" + """Output type (supported: ``label`` or ``dataframe``).""" label_name: Optional[str] = None - """TBD""" + """Label name (for label outputs only).""" table_name: Optional[str] = None - """TBD""" + """Table name (for dataframe outputs only).""" @validator("label_name", always=True) def label_name_only_for_label_type(cls, v, values): + """ + Check that label_name is set only for label outputs. + """ _type = values.get("type") if (_type == "label" and (not v)) or (_type != "label" and v): raise ValueError( @@ -106,6 +115,9 @@ def label_name_only_for_label_type(cls, v, values): @validator("table_name", always=True) def table_name_only_for_dataframe_type(cls, v, values): + """ + Check that table_name is set only for dataframe outputs. + """ _type = values.get("type") if (_type == "dataframe" and (not v)) or (_type != "dataframe" and v): raise ValueError( From 78579e38286cabdb9d843fb8e8a0b0670a28d6eb Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Wed, 21 Jun 2023 16:43:24 +0200 Subject: [PATCH 16/36] Update manifest --- fractal_tasks_core/__FRACTAL_MANIFEST__.json | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json index 3a0f097bc..cb35e034d 100644 --- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json +++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json @@ -77,7 +77,7 @@ "definitions": { "Window": { "title": "Window", - "description": "Custom class for Omero-channel window, related to OME-NGFF v0.4\n\nSee https://ngff.openmicroscopy.org/0.4/#omero-md.\nMain difference from the specs:\n\n 1. ``min`` and ``max`` are optional, since we have custom logic to set\n their values.", + "description": "Custom class for Omero-channel window, related to OME-NGFF v0.4\n\nSee https://ngff.openmicroscopy.org/0.4/#omero-md.\nMain difference from the specs:\n\n 1. ``min`` and ``max`` are optional, since we have custom logic to set\n their values.", "type": "object", "properties": { "min": { @@ -104,7 +104,7 @@ }, "OmeroChannel": { "title": "OmeroChannel", - "description": "Custom class for Omero channels, related to OME-NGFF v0.4.\n\nDifferences from OME-NGFF v0.4 specs\n(https://ngff.openmicroscopy.org/0.4/#omero-md)\n\n 1. Additional attributes ``wavelength_id`` and ``index``.\n 2. We make ``color`` an optional attribute, since we have custom\n logic to set its value.\n 3. We make ``window`` an optional attribute, so that we can also\n process zarr arrays which do not have this attribute.", + "description": "Custom class for Omero channels, related to OME-NGFF v0.4.\n\nDifferences from OME-NGFF v0.4 specs\n(https://ngff.openmicroscopy.org/0.4/#omero-md):\n\n 1. Additional attributes ``wavelength_id`` and ``index``.\n 2. We make ``color`` an optional attribute, since we have custom\n logic to set its value.\n 3. We make ``window`` an optional attribute, so that we can also\n process zarr arrays which do not have this attribute.", "type": "object", "properties": { "wavelength_id": { @@ -115,6 +115,10 @@ "title": "Index", "type": "integer" }, + "label": { + "title": "Label", + "type": "string" + }, "window": { "$ref": "#/definitions/Window" }, @@ -122,10 +126,6 @@ "title": "Color", "type": "string" }, - "label": { - "title": "Label", - "type": "string" - }, "active": { "title": "Active", "default": true, @@ -804,7 +804,7 @@ "definitions": { "Window": { "title": "Window", - "description": "Custom class for Omero-channel window, related to OME-NGFF v0.4\n\nSee https://ngff.openmicroscopy.org/0.4/#omero-md.\nMain difference from the specs:\n\n 1. ``min`` and ``max`` are optional, since we have custom logic to set\n their values.", + "description": "Custom class for Omero-channel window, related to OME-NGFF v0.4\n\nSee https://ngff.openmicroscopy.org/0.4/#omero-md.\nMain difference from the specs:\n\n 1. ``min`` and ``max`` are optional, since we have custom logic to set\n their values.", "type": "object", "properties": { "min": { @@ -831,7 +831,7 @@ }, "OmeroChannel": { "title": "OmeroChannel", - "description": "Custom class for Omero channels, related to OME-NGFF v0.4.\n\nDifferences from OME-NGFF v0.4 specs\n(https://ngff.openmicroscopy.org/0.4/#omero-md)\n\n 1. Additional attributes ``wavelength_id`` and ``index``.\n 2. We make ``color`` an optional attribute, since we have custom\n logic to set its value.\n 3. We make ``window`` an optional attribute, so that we can also\n process zarr arrays which do not have this attribute.", + "description": "Custom class for Omero channels, related to OME-NGFF v0.4.\n\nDifferences from OME-NGFF v0.4 specs\n(https://ngff.openmicroscopy.org/0.4/#omero-md):\n\n 1. Additional attributes ``wavelength_id`` and ``index``.\n 2. We make ``color`` an optional attribute, since we have custom\n logic to set its value.\n 3. We make ``window`` an optional attribute, so that we can also\n process zarr arrays which do not have this attribute.", "type": "object", "properties": { "wavelength_id": { @@ -842,6 +842,10 @@ "title": "Index", "type": "integer" }, + "label": { + "title": "Label", + "type": "string" + }, "window": { "$ref": "#/definitions/Window" }, @@ -849,10 +853,6 @@ "title": "Color", "type": "string" }, - "label": { - "title": "Label", - "type": "string" - }, "active": { "title": "Active", "default": true, From 932aa6ba8975bcdd2711a677649d58a6b0aaea6d Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Wed, 21 Jun 2023 16:53:16 +0200 Subject: [PATCH 17/36] Minor fix to docs config --- docs/source/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 63f7e1a2a..bd9e635dd 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -29,8 +29,9 @@ "private-members": True, "show-inheritance": True, "autosummary-no-nesting": True, + "autosummary_no_nesting": True, # valid options: bysource, groupwise, alphabetical - "autodoc_member_order": "bysource", + "autodoc_member_order": "groupwise", } autodata_content = "both" source_suffix = ".rst" From daed7e1cafd9065b6fda068becb89e29b2b3969b Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Wed, 21 Jun 2023 17:44:48 +0200 Subject: [PATCH 18/36] implement _get_function_args_descriptions --- fractal_tasks_core/dev/lib_descriptions.py | 20 +++++++++++++++++--- tests/test_unit_lib_descriptions.py | 11 ++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index d72862620..91401cc77 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -32,13 +32,27 @@ def _get_function_args_descriptions( package_name: str, module_relative_path: str, function_name: str ) -> dict[str, str]: + """ + Extract argument descriptions from a function + """ if not module_relative_path.endswith(".py"): raise ValueError(f"Module {module_relative_path} must end with '.py'") + else: + module_relative_path = module_relative_path.rstrip(".py") + module_name = Path(package_name).with_suffix("").name + module = import_module(f"{module_name}.{module_relative_path}") + function = getattr(module, function_name) - module = import_module(f"{package_name}.{module_relative_path}") - x = getattr(module, function_name) - return x + docstring = function.__doc__ + + # Parse docstring (via docstring_parser) and prepare output + parsed_docstring = docparse(docstring) + descriptions = { + param.arg_name: param.description.replace("\n", " ") + for param in parsed_docstring.params + } + return descriptions def _get_args_descriptions(executable) -> dict[str, str]: diff --git a/tests/test_unit_lib_descriptions.py b/tests/test_unit_lib_descriptions.py index 4dd989bdf..1fa695553 100644 --- a/tests/test_unit_lib_descriptions.py +++ b/tests/test_unit_lib_descriptions.py @@ -14,13 +14,14 @@ ) -def test_get_args_model_descriptions(): +def test_descriptions(): - x = _get_function_args_descriptions( - "fractal_tasks_core", "lib_input_models.py", "Channel" + function_doc = _get_function_args_descriptions( + "fractal_tasks_core", + "dev.lib_signature_constraints.py", + "_extract_function", ) - debug(x, dir(x)) - # assert False + assert function_doc.keys() == set(("executable", "package")) with pytest.raises(ValueError): _get_attributes_models_descriptions(models={"Foo": "__init__.py"}) From 4a4b993f5c9d5cc182785a72b93110f674dfafb4 Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Wed, 21 Jun 2023 17:45:48 +0200 Subject: [PATCH 19/36] separate tests --- tests/test_unit_lib_descriptions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_unit_lib_descriptions.py b/tests/test_unit_lib_descriptions.py index 1fa695553..bb04c6808 100644 --- a/tests/test_unit_lib_descriptions.py +++ b/tests/test_unit_lib_descriptions.py @@ -14,8 +14,7 @@ ) -def test_descriptions(): - +def test_get_function_args_descriptions(): function_doc = _get_function_args_descriptions( "fractal_tasks_core", "dev.lib_signature_constraints.py", @@ -23,6 +22,9 @@ def test_descriptions(): ) assert function_doc.keys() == set(("executable", "package")) + +def test_descriptions(): + with pytest.raises(ValueError): _get_attributes_models_descriptions(models={"Foo": "__init__.py"}) From b3dd0b2cc1134b96107823781ef72c8cb964f5cf Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Wed, 21 Jun 2023 18:10:17 +0200 Subject: [PATCH 20/36] implement _get_class_attrs_descriptions and test --- fractal_tasks_core/dev/lib_descriptions.py | 52 +++++++++++++--------- tests/test_unit_lib_descriptions.py | 14 +++++- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index 91401cc77..6aaaa782e 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -6,28 +6,6 @@ import fractal_tasks_core -# import inspect -# from inspect import signature -# from typing import Callable - - -""" -pkg_name = fractal_task_core -relative_module_path = lib_channels.py -object_name = {class_name} / {function_name} - - -_get_function_args_descriptions: dict[str, str] -_get_class_attrs_descriptions: dict[str, str] - -l'import deve essere fatto tipo: - module = import_module(f"{package}.{module_name}") - task_function = getattr(module, module_name) - -Per una funzione deve ritornare quello che già abbiamo -Per una classe dobbiamo fare quello che facciamo ma per una sola classe -""" - def _get_function_args_descriptions( package_name: str, module_relative_path: str, function_name: str @@ -55,6 +33,36 @@ def _get_function_args_descriptions( return descriptions +def _get_class_attrs_descriptions( + package_name: str, module_relative_path: str, class_name: str +) -> dict[str, str]: + if not module_relative_path.endswith(".py"): + raise ValueError(f"Module {module_relative_path} must end with '.py'") + + # get the class + module_path = Path(package_name) / module_relative_path + tree = ast.parse(module_path.read_text()) + + _class = next( + c + for c in ast.walk(tree) + if (isinstance(c, ast.ClassDef) and c.name == class_name) + ) + attrs_descriptions = {} + + # extract attribute docstrings + var_name: str = "" + for node in _class.body: + if isinstance(node, ast.AnnAssign): + attrs_descriptions[node.target.id] = "Missing description" + var_name = node.target.id + else: + if isinstance(node, ast.Expr) and var_name: + attrs_descriptions[var_name] = node.value.s + var_name = "" + return attrs_descriptions + + def _get_args_descriptions(executable) -> dict[str, str]: """ Extract argument descriptions for a task function. diff --git a/tests/test_unit_lib_descriptions.py b/tests/test_unit_lib_descriptions.py index bb04c6808..d0fc49bca 100644 --- a/tests/test_unit_lib_descriptions.py +++ b/tests/test_unit_lib_descriptions.py @@ -6,6 +6,9 @@ from fractal_tasks_core.dev.lib_descriptions import ( _get_attributes_models_descriptions, ) +from fractal_tasks_core.dev.lib_descriptions import ( + _get_class_attrs_descriptions, +) from fractal_tasks_core.dev.lib_descriptions import ( _get_function_args_descriptions, ) @@ -15,12 +18,19 @@ def test_get_function_args_descriptions(): - function_doc = _get_function_args_descriptions( + args_descriptions = _get_function_args_descriptions( "fractal_tasks_core", "dev.lib_signature_constraints.py", "_extract_function", ) - assert function_doc.keys() == set(("executable", "package")) + assert args_descriptions.keys() == set(("executable", "package")) + + +def test_get_class_attrs_descriptions(): + attrs_descriptions = _get_class_attrs_descriptions( + "fractal_tasks_core", "lib_input_models.py", "Channel" + ) + assert attrs_descriptions.keys() == set(("wavelength_id", "label")) def test_descriptions(): From 77cb15a8607c968d02683e005a1564df84767a1e Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Thu, 22 Jun 2023 10:16:25 +0200 Subject: [PATCH 21/36] clean lib_descriptions --- fractal_tasks_core/dev/lib_args_schemas.py | 9 ++ fractal_tasks_core/dev/lib_descriptions.py | 103 +++--------------- tests/{ => dev}/test_unit_lib_descriptions.py | 42 ++++--- 3 files changed, 47 insertions(+), 107 deletions(-) rename tests/{ => dev}/test_unit_lib_descriptions.py (52%) diff --git a/fractal_tasks_core/dev/lib_args_schemas.py b/fractal_tasks_core/dev/lib_args_schemas.py index 27a93b123..e9fe3b11a 100644 --- a/fractal_tasks_core/dev/lib_args_schemas.py +++ b/fractal_tasks_core/dev/lib_args_schemas.py @@ -34,6 +34,15 @@ _Schema = dict[str, Any] +INNER_PYDANTIC_MODELS = { + "OmeroChannel": "lib_channels.py", + "Window": "lib_channels.py", + "Channel": "lib_input_models.py", + "NapariWorkflowsInput": "lib_input_models.py", + "NapariWorkflowsOutput": "lib_input_models.py", +} + + def _remove_args_kwargs_properties(old_schema: _Schema) -> _Schema: """ Remove ``args`` and ``kwargs`` schema properties diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index 6aaaa782e..7ea4d7d64 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -1,11 +1,8 @@ import ast -from importlib import import_module from pathlib import Path from docstring_parser import parse as docparse -import fractal_tasks_core - def _get_function_args_descriptions( package_name: str, module_relative_path: str, function_name: str @@ -16,13 +13,17 @@ def _get_function_args_descriptions( if not module_relative_path.endswith(".py"): raise ValueError(f"Module {module_relative_path} must end with '.py'") - else: - module_relative_path = module_relative_path.rstrip(".py") - module_name = Path(package_name).with_suffix("").name - module = import_module(f"{module_name}.{module_relative_path}") - function = getattr(module, function_name) - docstring = function.__doc__ + # get the function + module_path = Path(package_name) / module_relative_path + tree = ast.parse(module_path.read_text()) + + _function = next( + f + for f in ast.walk(tree) + if (isinstance(f, ast.FunctionDef) and f.name == function_name) + ) + docstring = ast.get_docstring(_function) # Parse docstring (via docstring_parser) and prepare output parsed_docstring = docparse(docstring) @@ -36,6 +37,7 @@ def _get_function_args_descriptions( def _get_class_attrs_descriptions( package_name: str, module_relative_path: str, class_name: str ) -> dict[str, str]: + if not module_relative_path.endswith(".py"): raise ValueError(f"Module {module_relative_path} must end with '.py'") @@ -48,46 +50,22 @@ def _get_class_attrs_descriptions( for c in ast.walk(tree) if (isinstance(c, ast.ClassDef) and c.name == class_name) ) - attrs_descriptions = {} + descriptions = {} # extract attribute docstrings var_name: str = "" for node in _class.body: if isinstance(node, ast.AnnAssign): - attrs_descriptions[node.target.id] = "Missing description" + descriptions[node.target.id] = "Missing description" var_name = node.target.id else: if isinstance(node, ast.Expr) and var_name: - attrs_descriptions[var_name] = node.value.s + descriptions[var_name] = node.value.s var_name = "" - return attrs_descriptions - - -def _get_args_descriptions(executable) -> dict[str, str]: - """ - Extract argument descriptions for a task function. - """ - # Read docstring (via ast) - module_path = Path(fractal_tasks_core.__file__).parent / executable - module_name = module_path.with_suffix("").name - tree = ast.parse(module_path.read_text()) - function = next( - f - for f in ast.walk(tree) - if (isinstance(f, ast.FunctionDef) and f.name == module_name) - ) - - docstring = ast.get_docstring(function) - # Parse docstring (via docstring_parser) and prepare output - parsed_docstring = docparse(docstring) - descriptions = { - param.arg_name: param.description.replace("\n", " ") - for param in parsed_docstring.params - } return descriptions -def _include_args_descriptions_in_schema(*, schema, descriptions): +def _insert_function_args_descriptions(*, schema, descriptions): """ Merge the descriptions obtained via `_get_args_descriptions` into an existing JSON Schema for task arguments. @@ -107,52 +85,7 @@ def _include_args_descriptions_in_schema(*, schema, descriptions): return new_schema -INNER_PYDANTIC_MODELS = { - "OmeroChannel": "lib_channels.py", - "Window": "lib_channels.py", - "Channel": "lib_input_models.py", - "NapariWorkflowsInput": "lib_input_models.py", - "NapariWorkflowsOutput": "lib_input_models.py", -} - - -def _get_attributes_models_descriptions( - models: dict[str, str] = INNER_PYDANTIC_MODELS -) -> dict[str, dict[str, str]]: - """ - Extract attribut descriptions for Pydantic models - """ - descriptions = {} - - for model, module in models.items(): - # get the class - module_path = Path(fractal_tasks_core.__file__).parent / module - tree = ast.parse(module_path.read_text()) - try: - _class = next( - c - for c in ast.walk(tree) - if (isinstance(c, ast.ClassDef) and c.name == model) - ) - descriptions[model] = {} - except StopIteration: - raise ValueError(f"Model {module_path}::{model} not found.") - - # extract attribute docstrings - var_name: str = "" - for node in _class.body: - if isinstance(node, ast.AnnAssign): - descriptions[model][node.target.id] = "Missing description" - var_name = node.target.id - else: - if isinstance(node, ast.Expr) and var_name: - descriptions[model][var_name] = node.value.s - var_name = "" - - return descriptions - - -def _include_attributs_descriptions_in_schema(*, schema, descriptions): +def _insert_class_attrs_descriptions(*, schema, class_name, descriptions): """ Merge the descriptions obtained via `_get_attributes_models_descriptions` into an existing JSON Schema for task arguments. @@ -165,7 +98,7 @@ def _include_attributs_descriptions_in_schema(*, schema, descriptions): new_definitions = schema["definitions"].copy() for name, definition in schema["definitions"].items(): - if name in descriptions.keys(): + if name == class_name: for prop in definition["properties"]: if "description" in new_definitions[name]["properties"][prop]: raise ValueError( @@ -174,6 +107,6 @@ def _include_attributs_descriptions_in_schema(*, schema, descriptions): else: new_definitions[name]["properties"][prop][ "description" - ] = descriptions[name][prop] + ] = descriptions[prop] new_schema["definitions"] = new_definitions return new_schema diff --git a/tests/test_unit_lib_descriptions.py b/tests/dev/test_unit_lib_descriptions.py similarity index 52% rename from tests/test_unit_lib_descriptions.py rename to tests/dev/test_unit_lib_descriptions.py index d0fc49bca..5a9dae9d8 100644 --- a/tests/test_unit_lib_descriptions.py +++ b/tests/dev/test_unit_lib_descriptions.py @@ -1,11 +1,7 @@ import json -import pytest from devtools import debug -from fractal_tasks_core.dev.lib_descriptions import ( - _get_attributes_models_descriptions, -) from fractal_tasks_core.dev.lib_descriptions import ( _get_class_attrs_descriptions, ) @@ -13,16 +9,21 @@ _get_function_args_descriptions, ) from fractal_tasks_core.dev.lib_descriptions import ( - _include_attributs_descriptions_in_schema, + _insert_class_attrs_descriptions, ) +# from fractal_tasks_core.dev.lib_descriptions import ( +# _insert_function_args_descriptions, +# ) + def test_get_function_args_descriptions(): args_descriptions = _get_function_args_descriptions( "fractal_tasks_core", - "dev.lib_signature_constraints.py", + "dev/lib_signature_constraints.py", "_extract_function", ) + debug(args_descriptions) assert args_descriptions.keys() == set(("executable", "package")) @@ -30,32 +31,29 @@ def test_get_class_attrs_descriptions(): attrs_descriptions = _get_class_attrs_descriptions( "fractal_tasks_core", "lib_input_models.py", "Channel" ) + debug(attrs_descriptions) assert attrs_descriptions.keys() == set(("wavelength_id", "label")) def test_descriptions(): + FILE = "lib_channels.py" + CLASS = "OmeroChannel" - with pytest.raises(ValueError): - _get_attributes_models_descriptions(models={"Foo": "__init__.py"}) - - descriptions = _get_attributes_models_descriptions() - debug(descriptions) + descriptions = _get_class_attrs_descriptions( + "fractal_tasks_core", FILE, CLASS + ) with open("fractal_tasks_core/__FRACTAL_MANIFEST__.json", "r") as f: manifest = json.load(f) schemas = [task["args_schema"] for task in manifest["task_list"]] - for i, schema in enumerate(schemas): - new_schema = _include_attributs_descriptions_in_schema( - schema=schema, descriptions=descriptions + for _, schema in enumerate(schemas): + new_schema = _insert_class_attrs_descriptions( + schema=schema, class_name=CLASS, descriptions=descriptions ) if "definitions" in schema: - for _, definition in new_schema["definitions"].items(): - for prop in definition["properties"]: - assert "description" in definition["properties"][prop] - - with pytest.raises(ValueError): - _include_attributs_descriptions_in_schema( - schema=new_schema, descriptions=descriptions - ) + for class_name, definition in new_schema["definitions"].items(): + if class_name == "OmeroChannel": + for prop in definition["properties"]: + assert "description" in definition["properties"][prop] From 612ea70b04e4b60db7bc06b474a532baec1c0cb8 Mon Sep 17 00:00:00 2001 From: Yuri Chiucconi Date: Thu, 22 Jun 2023 11:00:40 +0200 Subject: [PATCH 22/36] changed lib_args_schemas --- fractal_tasks_core/dev/lib_args_schemas.py | 50 ++++++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/fractal_tasks_core/dev/lib_args_schemas.py b/fractal_tasks_core/dev/lib_args_schemas.py index e9fe3b11a..d974f2f0c 100644 --- a/fractal_tasks_core/dev/lib_args_schemas.py +++ b/fractal_tasks_core/dev/lib_args_schemas.py @@ -15,15 +15,24 @@ """ from typing import Any +import Path from pydantic.decorator import ALT_V_ARGS from pydantic.decorator import ALT_V_KWARGS from pydantic.decorator import V_DUPLICATE_KWARGS from pydantic.decorator import V_POSITIONAL_ONLY_NAME from pydantic.decorator import ValidatedFunction -from fractal_tasks_core.dev.lib_descriptions import _get_args_descriptions from fractal_tasks_core.dev.lib_descriptions import ( - _include_args_descriptions_in_schema, + _get_class_attrs_descriptions, +) +from fractal_tasks_core.dev.lib_descriptions import ( + _get_function_args_descriptions, +) +from fractal_tasks_core.dev.lib_descriptions import ( + _insert_class_attrs_descriptions, +) +from fractal_tasks_core.dev.lib_descriptions import ( + _insert_function_args_descriptions, ) from fractal_tasks_core.dev.lib_signature_constraints import _extract_function from fractal_tasks_core.dev.lib_signature_constraints import ( @@ -34,15 +43,6 @@ _Schema = dict[str, Any] -INNER_PYDANTIC_MODELS = { - "OmeroChannel": "lib_channels.py", - "Window": "lib_channels.py", - "Channel": "lib_input_models.py", - "NapariWorkflowsInput": "lib_input_models.py", - "NapariWorkflowsOutput": "lib_input_models.py", -} - - def _remove_args_kwargs_properties(old_schema: _Schema) -> _Schema: """ Remove ``args`` and ``kwargs`` schema properties @@ -107,9 +107,31 @@ def create_schema_for_single_task( schema = _remove_pydantic_internals(schema) # Include arg descriptions - descriptions = _get_args_descriptions(executable) - schema = _include_args_descriptions_in_schema( - schema=schema, descriptions=descriptions + function_name = Path(executable).with_suffix("").name + function_args_descriptions = _get_function_args_descriptions( + package_name=package, + module_relative_path=executable, + function_name=function_name, + ) + schema = _insert_function_args_descriptions( + schema=schema, descriptions=function_args_descriptions ) + # Include inner Pydantic models attrs descriprions + INNER_PYDANTIC_MODELS = { + "OmeroChannel": "lib_channels.py", + "Window": "lib_channels.py", + "Channel": "lib_input_models.py", + "NapariWorkflowsInput": "lib_input_models.py", + "NapariWorkflowsOutput": "lib_input_models.py", + } + for _class, module in INNER_PYDANTIC_MODELS.items(): + descriptions = _get_class_attrs_descriptions( + package_name=package, + module_relative_path=module, + class_name=_class, + ) + schema = _insert_class_attrs_descriptions( + schema=schema, class_name=_class, descriptions=descriptions + ) return schema From 02f5437eb21941b16f65787479a6641c7d5584a2 Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 11:10:08 +0200 Subject: [PATCH 23/36] Clean up lib_descriptions.py --- fractal_tasks_core/dev/lib_descriptions.py | 23 +++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index 7ea4d7d64..9f96c9340 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -9,6 +9,12 @@ def _get_function_args_descriptions( ) -> dict[str, str]: """ Extract argument descriptions from a function + + TODO: use ast instead of import, as in _get_class_attrs_descriptions + + :param package_name: Example ``fractal_tasks_core`` + :param module_relative_path: Example ``tasks/create_ome_zarr.py`` + :param function_name: Example ``create_ome_zarr`` """ if not module_relative_path.endswith(".py"): @@ -37,6 +43,13 @@ def _get_function_args_descriptions( def _get_class_attrs_descriptions( package_name: str, module_relative_path: str, class_name: str ) -> dict[str, str]: + """ + Extract attribute descriptions from a class + + :param package_name: Example ``fractal_tasks_core`` + :param module_relative_path: Example ``lib_channels.py`` + :param class_name: Example ``OmeroChannel`` + """ if not module_relative_path.endswith(".py"): raise ValueError(f"Module {module_relative_path} must end with '.py'") @@ -67,8 +80,9 @@ def _get_class_attrs_descriptions( def _insert_function_args_descriptions(*, schema, descriptions): """ - Merge the descriptions obtained via `_get_args_descriptions` into an - existing JSON Schema for task arguments. + Merge the descriptions obtained via `_get_args_descriptions` into the + properties of an existing JSON Schema. + """ new_schema = schema.copy() new_properties = schema["properties"].copy() @@ -88,15 +102,14 @@ def _insert_function_args_descriptions(*, schema, descriptions): def _insert_class_attrs_descriptions(*, schema, class_name, descriptions): """ Merge the descriptions obtained via `_get_attributes_models_descriptions` - into an existing JSON Schema for task arguments. + into the ``class_name`` definition, within an existing JSON Schema """ new_schema = schema.copy() - if "definitions" not in schema: return new_schema else: new_definitions = schema["definitions"].copy() - + # Loop over existing definitions for name, definition in schema["definitions"].items(): if name == class_name: for prop in definition["properties"]: From f50d793edcb0c9765daf35af1ee2d2ca5a84590c Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:13:47 +0200 Subject: [PATCH 24/36] Update extract_function, and make its interface compatible with functions in lib_descriptions.py --- .../dev/lib_signature_constraints.py | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/fractal_tasks_core/dev/lib_signature_constraints.py b/fractal_tasks_core/dev/lib_signature_constraints.py index fc28f05cd..dd68f9967 100644 --- a/fractal_tasks_core/dev/lib_signature_constraints.py +++ b/fractal_tasks_core/dev/lib_signature_constraints.py @@ -3,6 +3,7 @@ from inspect import signature from pathlib import Path from typing import Callable +from typing import Optional from pydantic.decorator import ALT_V_ARGS from pydantic.decorator import ALT_V_KWARGS @@ -20,25 +21,25 @@ def _extract_function( - executable: str, - package: str = "fractal_tasks_core.tasks", + module_relative_path: str, + package_name: str = "fractal_tasks_core", + function_name: Optional[str] = None, ) -> Callable: """ Extract function from a module with the same name - This for instance extracts the function `my_function` from the module - `my_function.py`. - - :param executable: Path to Python task script. Valid examples: - `tasks/my_function.py` or `my_function.py`. - :param package: Name of the tasks subpackage (e.g. - `fractal_tasks_core.tasks`). + :param package_name: Example ``fractal_tasks_core`` + :param module_relative_path: Example ``tasks/create_ome_zarr.py`` + :param function_name: Example ``create_ome_zarr`` """ - if not executable.endswith(".py"): - raise ValueError(f"{executable=} must end with '.py'") - module_name = Path(executable).with_suffix("").name - module = import_module(f"{package}.{module_name}") - task_function = getattr(module, module_name) + if not module_relative_path.endswith(".py"): + raise ValueError(f"{module_relative_path=} must end with '.py'") + module_relative_path_no_py = str( + Path(module_relative_path).with_suffix("") + ) + module_relative_path_dots = module_relative_path_no_py.replace("/", ".") + module = import_module(f"{package_name}.{module_relative_path_dots}") + task_function = getattr(module, function_name) return task_function From 7c428fe29d9c3df1101731c7b0ae11a90690ec90 Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:14:05 +0200 Subject: [PATCH 25/36] Fix setting of `module_path` in lib_descriptions.py --- fractal_tasks_core/dev/lib_descriptions.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index 9f96c9340..25e03a208 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -1,4 +1,5 @@ import ast +from importlib import import_module from pathlib import Path from docstring_parser import parse as docparse @@ -20,15 +21,17 @@ def _get_function_args_descriptions( if not module_relative_path.endswith(".py"): raise ValueError(f"Module {module_relative_path} must end with '.py'") - # get the function - module_path = Path(package_name) / module_relative_path + # Get the function ast.FunctionDef object + package_path = Path(import_module(package_name).__file__).parent + module_path = package_path / module_relative_path tree = ast.parse(module_path.read_text()) - _function = next( f for f in ast.walk(tree) if (isinstance(f, ast.FunctionDef) and f.name == function_name) ) + + # Extract docstring from ast.FunctionDef docstring = ast.get_docstring(_function) # Parse docstring (via docstring_parser) and prepare output @@ -54,10 +57,10 @@ def _get_class_attrs_descriptions( if not module_relative_path.endswith(".py"): raise ValueError(f"Module {module_relative_path} must end with '.py'") - # get the class - module_path = Path(package_name) / module_relative_path + # Get the class ast.ClassDef object + package_path = Path(import_module(package_name).__file__).parent + module_path = package_path / module_relative_path tree = ast.parse(module_path.read_text()) - _class = next( c for c in ast.walk(tree) From 1a1a7e905fe28a0221f725df13e11e6780c3124e Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:18:22 +0200 Subject: [PATCH 26/36] Fix missing import and update create_schema_for_single_task interface --- fractal_tasks_core/dev/lib_args_schemas.py | 34 +++++++++++++++------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/fractal_tasks_core/dev/lib_args_schemas.py b/fractal_tasks_core/dev/lib_args_schemas.py index d974f2f0c..14c329e43 100644 --- a/fractal_tasks_core/dev/lib_args_schemas.py +++ b/fractal_tasks_core/dev/lib_args_schemas.py @@ -5,6 +5,7 @@ Original authors: Tommaso Comparin + Yuri Chiucconi This file is part of Fractal and was originally developed by eXact lab S.r.l. under contract with Liberali Lab from the Friedrich @@ -13,9 +14,9 @@ Helper functions to handle JSON schemas for task arguments. """ +from pathlib import Path from typing import Any -import Path from pydantic.decorator import ALT_V_ARGS from pydantic.decorator import ALT_V_KWARGS from pydantic.decorator import V_DUPLICATE_KWARGS @@ -42,6 +43,14 @@ _Schema = dict[str, Any] +INNER_PYDANTIC_MODELS = { + "OmeroChannel": "lib_channels.py", + "Window": "lib_channels.py", + "Channel": "lib_input_models.py", + "NapariWorkflowsInput": "lib_input_models.py", + "NapariWorkflowsOutput": "lib_input_models.py", +} + def _remove_args_kwargs_properties(old_schema: _Schema) -> _Schema: """ @@ -89,13 +98,23 @@ def _remove_pydantic_internals(old_schema: _Schema) -> _Schema: def create_schema_for_single_task( executable: str, - package: str = "fractal_tasks_core.tasks", + package: str = "fractal_tasks_core", + inner_pydantic_models: dict[str, str] = INNER_PYDANTIC_MODELS, ) -> _Schema: """ Main function to create a JSON Schema of task arguments """ + + # Extract the function name. Note: this could be made more general, but for + # the moment we assume the function has the same name as the module) + function_name = Path(executable).with_suffix("").name + # Extract function from module - task_function = _extract_function(executable=executable, package=package) + task_function = _extract_function( + package_name=package, + module_relative_path=executable, + function_name=function_name, + ) # Validate function signature against some custom constraints _validate_function_signature(task_function) @@ -107,7 +126,6 @@ def create_schema_for_single_task( schema = _remove_pydantic_internals(schema) # Include arg descriptions - function_name = Path(executable).with_suffix("").name function_args_descriptions = _get_function_args_descriptions( package_name=package, module_relative_path=executable, @@ -117,13 +135,7 @@ def create_schema_for_single_task( schema=schema, descriptions=function_args_descriptions ) # Include inner Pydantic models attrs descriprions - INNER_PYDANTIC_MODELS = { - "OmeroChannel": "lib_channels.py", - "Window": "lib_channels.py", - "Channel": "lib_input_models.py", - "NapariWorkflowsInput": "lib_input_models.py", - "NapariWorkflowsOutput": "lib_input_models.py", - } + for _class, module in INNER_PYDANTIC_MODELS.items(): descriptions = _get_class_attrs_descriptions( package_name=package, From 4eb8334522d08abd296c785b7e9807e5dc9a3da5 Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:18:38 +0200 Subject: [PATCH 27/36] Improve error-handling in new_args_schema.py --- fractal_tasks_core/dev/new_args_schema.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/fractal_tasks_core/dev/new_args_schema.py b/fractal_tasks_core/dev/new_args_schema.py index daee63869..33bea8a57 100644 --- a/fractal_tasks_core/dev/new_args_schema.py +++ b/fractal_tasks_core/dev/new_args_schema.py @@ -44,10 +44,8 @@ print(f"[{executable}] Start") try: schema = create_schema_for_single_task(executable) - except AttributeError: - print(f"[{executable}] Skip, due to AttributeError") - print() - continue + except Exception as e: + print(f"[{executable}] Skip. Original error:\n{str(e)}") manifest["task_list"][ind]["args_schema"] = schema print("Schema added to manifest") From b428e94e1fbdf3c4f1a0e29659d906845239013b Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:18:52 +0200 Subject: [PATCH 28/36] Include class-attributes descriptions in manifest --- fractal_tasks_core/__FRACTAL_MANIFEST__.json | 102 ++++++++++++------- 1 file changed, 68 insertions(+), 34 deletions(-) diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json index cb35e034d..99dae8b44 100644 --- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json +++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json @@ -82,19 +82,23 @@ "properties": { "min": { "title": "Min", - "type": "integer" + "type": "integer", + "description": "If not provided, it will be set to ``0``." }, "max": { "title": "Max", - "type": "integer" + "type": "integer", + "description": "If not provided, it will be set according to bit-depth." }, "start": { "title": "Start", - "type": "integer" + "type": "integer", + "description": "Start value of the visualization window." }, "end": { "title": "End", - "type": "integer" + "type": "integer", + "description": "End value of the visualization window." } }, "required": [ @@ -109,37 +113,45 @@ "properties": { "wavelength_id": { "title": "Wavelength Id", - "type": "string" + "type": "string", + "description": "Custom attribute (e.g. ``A01_C01``)." }, "index": { "title": "Index", - "type": "integer" + "type": "integer", + "description": "For internal use only." }, "label": { "title": "Label", - "type": "string" + "type": "string", + "description": "Channel label." }, "window": { - "$ref": "#/definitions/Window" + "$ref": "#/definitions/Window", + "description": "A ``Window`` object to display this channel in napari." }, "color": { "title": "Color", - "type": "string" + "type": "string", + "description": "A colormap to display the channel in napari (e.g. ``00FFFF``)." }, "active": { "title": "Active", "default": true, - "type": "boolean" + "type": "boolean", + "description": "Omero-channel attribute." }, "coefficient": { "title": "Coefficient", "default": 1, - "type": "integer" + "type": "integer", + "description": "Omero-channel attribute." }, "inverted": { "title": "Inverted", "default": false, - "type": "boolean" + "type": "boolean", + "description": "Omero-channel attribute." } }, "required": [ @@ -462,11 +474,13 @@ "properties": { "wavelength_id": { "title": "Wavelength Id", - "type": "string" + "type": "string", + "description": "Unique ID for the channel wavelength, e.g. ``A01_C01``." }, "label": { "title": "Label", - "type": "string" + "type": "string", + "description": "Channel label." } } } @@ -654,11 +668,13 @@ "properties": { "wavelength_id": { "title": "Wavelength Id", - "type": "string" + "type": "string", + "description": "Unique ID for the channel wavelength, e.g. ``A01_C01``." }, "label": { "title": "Label", - "type": "string" + "type": "string", + "description": "Channel label." } } }, @@ -673,14 +689,17 @@ "image", "label" ], - "type": "string" + "type": "string", + "description": "Input type (supported: ``image`` or ``label``)." }, "label_name": { "title": "Label Name", - "type": "string" + "type": "string", + "description": "Label name (for label inputs only)." }, "channel": { - "$ref": "#/definitions/Channel" + "$ref": "#/definitions/Channel", + "description": "Channel object (for image inputs only)." } }, "required": [ @@ -698,15 +717,18 @@ "label", "dataframe" ], - "type": "string" + "type": "string", + "description": "Output type (supported: ``label`` or ``dataframe``)." }, "label_name": { "title": "Label Name", - "type": "string" + "type": "string", + "description": "Label name (for label outputs only)." }, "table_name": { "title": "Table Name", - "type": "string" + "type": "string", + "description": "Table name (for dataframe outputs only)." } }, "required": [ @@ -809,19 +831,23 @@ "properties": { "min": { "title": "Min", - "type": "integer" + "type": "integer", + "description": "If not provided, it will be set to ``0``." }, "max": { "title": "Max", - "type": "integer" + "type": "integer", + "description": "If not provided, it will be set according to bit-depth." }, "start": { "title": "Start", - "type": "integer" + "type": "integer", + "description": "Start value of the visualization window." }, "end": { "title": "End", - "type": "integer" + "type": "integer", + "description": "End value of the visualization window." } }, "required": [ @@ -836,37 +862,45 @@ "properties": { "wavelength_id": { "title": "Wavelength Id", - "type": "string" + "type": "string", + "description": "Custom attribute (e.g. ``A01_C01``)." }, "index": { "title": "Index", - "type": "integer" + "type": "integer", + "description": "For internal use only." }, "label": { "title": "Label", - "type": "string" + "type": "string", + "description": "Channel label." }, "window": { - "$ref": "#/definitions/Window" + "$ref": "#/definitions/Window", + "description": "A ``Window`` object to display this channel in napari." }, "color": { "title": "Color", - "type": "string" + "type": "string", + "description": "A colormap to display the channel in napari (e.g. ``00FFFF``)." }, "active": { "title": "Active", "default": true, - "type": "boolean" + "type": "boolean", + "description": "Omero-channel attribute." }, "coefficient": { "title": "Coefficient", "default": 1, - "type": "integer" + "type": "integer", + "description": "Omero-channel attribute." }, "inverted": { "title": "Inverted", "default": false, - "type": "boolean" + "type": "boolean", + "description": "Omero-channel attribute." } }, "required": [ From 2782f4cd0a084241515c8805224c2f1b6e3db7e7 Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:20:33 +0200 Subject: [PATCH 29/36] Remove obsolete comment and `from __future__ import annotations` --- fractal_tasks_core/tasks/_utils.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/fractal_tasks_core/tasks/_utils.py b/fractal_tasks_core/tasks/_utils.py index 5e408e6e2..a0657acd0 100644 --- a/fractal_tasks_core/tasks/_utils.py +++ b/fractal_tasks_core/tasks/_utils.py @@ -13,11 +13,6 @@ Standard input/output interface for tasks """ -# Starting from Python 3.9 (see PEP 585) we can use type hints like -# `type[BaseModel]`. For versions 3.7 and 3.8, this is available through an -# additional import -from __future__ import annotations - import json import logging from argparse import ArgumentParser From 11ccfc60ade5d27131d799b13482e7145db7ec62 Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:21:25 +0200 Subject: [PATCH 30/36] Remove TODO comment --- fractal_tasks_core/dev/lib_descriptions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py index 25e03a208..8bbc82b32 100644 --- a/fractal_tasks_core/dev/lib_descriptions.py +++ b/fractal_tasks_core/dev/lib_descriptions.py @@ -11,8 +11,6 @@ def _get_function_args_descriptions( """ Extract argument descriptions from a function - TODO: use ast instead of import, as in _get_class_attrs_descriptions - :param package_name: Example ``fractal_tasks_core`` :param module_relative_path: Example ``tasks/create_ome_zarr.py`` :param function_name: Example ``create_ome_zarr`` From 6f8d5ba2a872cb7a17476b8e1f3f381a3bc50805 Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:23:20 +0200 Subject: [PATCH 31/36] move blank line --- fractal_tasks_core/dev/lib_args_schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fractal_tasks_core/dev/lib_args_schemas.py b/fractal_tasks_core/dev/lib_args_schemas.py index 14c329e43..e4d7ce731 100644 --- a/fractal_tasks_core/dev/lib_args_schemas.py +++ b/fractal_tasks_core/dev/lib_args_schemas.py @@ -134,8 +134,8 @@ def create_schema_for_single_task( schema = _insert_function_args_descriptions( schema=schema, descriptions=function_args_descriptions ) - # Include inner Pydantic models attrs descriprions + # Include inner Pydantic models attrs descriprions for _class, module in INNER_PYDANTIC_MODELS.items(): descriptions = _get_class_attrs_descriptions( package_name=package, From 26a30b670f418b8b84ee6605ceee495aefd45d48 Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:28:34 +0200 Subject: [PATCH 32/36] Update test_unit_lib_descriptions.py 1. Remove `test_descriptions`, since it would always fail because descriptions are already in the manifest; 2. Update other tests. --- tests/dev/test_unit_lib_descriptions.py | 37 ++----------------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/tests/dev/test_unit_lib_descriptions.py b/tests/dev/test_unit_lib_descriptions.py index 5a9dae9d8..969bff985 100644 --- a/tests/dev/test_unit_lib_descriptions.py +++ b/tests/dev/test_unit_lib_descriptions.py @@ -1,5 +1,3 @@ -import json - from devtools import debug from fractal_tasks_core.dev.lib_descriptions import ( @@ -8,13 +6,6 @@ from fractal_tasks_core.dev.lib_descriptions import ( _get_function_args_descriptions, ) -from fractal_tasks_core.dev.lib_descriptions import ( - _insert_class_attrs_descriptions, -) - -# from fractal_tasks_core.dev.lib_descriptions import ( -# _insert_function_args_descriptions, -# ) def test_get_function_args_descriptions(): @@ -24,7 +15,9 @@ def test_get_function_args_descriptions(): "_extract_function", ) debug(args_descriptions) - assert args_descriptions.keys() == set(("executable", "package")) + assert args_descriptions.keys() == set( + ("package_name", "module_relative_path", "function_name") + ) def test_get_class_attrs_descriptions(): @@ -33,27 +26,3 @@ def test_get_class_attrs_descriptions(): ) debug(attrs_descriptions) assert attrs_descriptions.keys() == set(("wavelength_id", "label")) - - -def test_descriptions(): - FILE = "lib_channels.py" - CLASS = "OmeroChannel" - - descriptions = _get_class_attrs_descriptions( - "fractal_tasks_core", FILE, CLASS - ) - - with open("fractal_tasks_core/__FRACTAL_MANIFEST__.json", "r") as f: - manifest = json.load(f) - - schemas = [task["args_schema"] for task in manifest["task_list"]] - - for _, schema in enumerate(schemas): - new_schema = _insert_class_attrs_descriptions( - schema=schema, class_name=CLASS, descriptions=descriptions - ) - if "definitions" in schema: - for class_name, definition in new_schema["definitions"].items(): - if class_name == "OmeroChannel": - for prop in definition["properties"]: - assert "description" in definition["properties"][prop] From 7f31edd18354addf44fe88b4bd37f930b9a687d7 Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:31:28 +0200 Subject: [PATCH 33/36] Update test_valid_args_schemas.py --- tests/tasks/test_valid_args_schemas.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/tasks/test_valid_args_schemas.py b/tests/tasks/test_valid_args_schemas.py index 5cdd2cd4f..6c37ab6ce 100644 --- a/tests/tasks/test_valid_args_schemas.py +++ b/tests/tasks/test_valid_args_schemas.py @@ -99,7 +99,8 @@ def test_task_functions_have_valid_signatures(): Test that task functions have valid signatures. """ for ind_task, task in enumerate(TASK_LIST): - task_function = _extract_function(task["executable"]) + function_name = Path(task["executable"]).with_suffix("").name + task_function = _extract_function(task["executable"], function_name) _validate_function_signature(task_function) From 2fbe417f85ab1b281cac2614a702e2989d9a3b61 Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:31:45 +0200 Subject: [PATCH 34/36] Make _extract_function more strict --- fractal_tasks_core/dev/lib_signature_constraints.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fractal_tasks_core/dev/lib_signature_constraints.py b/fractal_tasks_core/dev/lib_signature_constraints.py index dd68f9967..22ac64edc 100644 --- a/fractal_tasks_core/dev/lib_signature_constraints.py +++ b/fractal_tasks_core/dev/lib_signature_constraints.py @@ -3,7 +3,6 @@ from inspect import signature from pathlib import Path from typing import Callable -from typing import Optional from pydantic.decorator import ALT_V_ARGS from pydantic.decorator import ALT_V_KWARGS @@ -22,8 +21,8 @@ def _extract_function( module_relative_path: str, + function_name: str, package_name: str = "fractal_tasks_core", - function_name: Optional[str] = None, ) -> Callable: """ Extract function from a module with the same name From e3f518ee4bffa172563d0c6c75597ee2f9f900bc Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:37:22 +0200 Subject: [PATCH 35/36] Update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 812ed5cb2..35e9829b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ * Modify arguments of `illumination_correction` task (\#431); * Modify arguments of `create_ome_zarr` and `create_ome_zarr_multiplex` (\#433). * JSON Schemas for task arguments: - * Add JSON schemas for task arguments in the package manifest (\#369, \#384). + * Add JSON Schemas for task arguments in the package manifest (\#369, \#384). + * Add JSON Schemas for attributes of custom task-argument Pydantic models (\#436). * Remove `TaskArguments` models and switch to Pydantic V1 `validate_arguments` (\#369). * Make coercing&validating task arguments required, rather than optional (\#408). * Remove `default_args` from manifest (\#379, \#393). From 2148cabe13b1414b37177fa8d836769ffde80359 Mon Sep 17 00:00:00 2001 From: Tommaso Comparin <3862206+tcompa@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:44:37 +0200 Subject: [PATCH 36/36] Simplify docstrings (ref #421) --- fractal_tasks_core/__FRACTAL_MANIFEST__.json | 16 +++++++------- fractal_tasks_core/lib_channels.py | 23 ++++---------------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json index 99dae8b44..baa1ed298 100644 --- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json +++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json @@ -77,7 +77,7 @@ "definitions": { "Window": { "title": "Window", - "description": "Custom class for Omero-channel window, related to OME-NGFF v0.4\n\nSee https://ngff.openmicroscopy.org/0.4/#omero-md.\nMain difference from the specs:\n\n 1. ``min`` and ``max`` are optional, since we have custom logic to set\n their values.", + "description": "Custom class for Omero-channel window, based on OME-NGFF v0.4.", "type": "object", "properties": { "min": { @@ -108,7 +108,7 @@ }, "OmeroChannel": { "title": "OmeroChannel", - "description": "Custom class for Omero channels, related to OME-NGFF v0.4.\n\nDifferences from OME-NGFF v0.4 specs\n(https://ngff.openmicroscopy.org/0.4/#omero-md):\n\n 1. Additional attributes ``wavelength_id`` and ``index``.\n 2. We make ``color`` an optional attribute, since we have custom\n logic to set its value.\n 3. We make ``window`` an optional attribute, so that we can also\n process zarr arrays which do not have this attribute.", + "description": "Custom class for Omero channels, based on OME-NGFF v0.4.", "type": "object", "properties": { "wavelength_id": { @@ -128,12 +128,12 @@ }, "window": { "$ref": "#/definitions/Window", - "description": "A ``Window`` object to display this channel in napari." + "description": "Optional ``Window`` object to display this channel in napari." }, "color": { "title": "Color", "type": "string", - "description": "A colormap to display the channel in napari (e.g. ``00FFFF``)." + "description": "Optional colormap to display the channel in napari (e.g. ``00FFFF``)." }, "active": { "title": "Active", @@ -826,7 +826,7 @@ "definitions": { "Window": { "title": "Window", - "description": "Custom class for Omero-channel window, related to OME-NGFF v0.4\n\nSee https://ngff.openmicroscopy.org/0.4/#omero-md.\nMain difference from the specs:\n\n 1. ``min`` and ``max`` are optional, since we have custom logic to set\n their values.", + "description": "Custom class for Omero-channel window, based on OME-NGFF v0.4.", "type": "object", "properties": { "min": { @@ -857,7 +857,7 @@ }, "OmeroChannel": { "title": "OmeroChannel", - "description": "Custom class for Omero channels, related to OME-NGFF v0.4.\n\nDifferences from OME-NGFF v0.4 specs\n(https://ngff.openmicroscopy.org/0.4/#omero-md):\n\n 1. Additional attributes ``wavelength_id`` and ``index``.\n 2. We make ``color`` an optional attribute, since we have custom\n logic to set its value.\n 3. We make ``window`` an optional attribute, so that we can also\n process zarr arrays which do not have this attribute.", + "description": "Custom class for Omero channels, based on OME-NGFF v0.4.", "type": "object", "properties": { "wavelength_id": { @@ -877,12 +877,12 @@ }, "window": { "$ref": "#/definitions/Window", - "description": "A ``Window`` object to display this channel in napari." + "description": "Optional ``Window`` object to display this channel in napari." }, "color": { "title": "Color", "type": "string", - "description": "A colormap to display the channel in napari (e.g. ``00FFFF``)." + "description": "Optional colormap to display the channel in napari (e.g. ``00FFFF``)." }, "active": { "title": "Active", diff --git a/fractal_tasks_core/lib_channels.py b/fractal_tasks_core/lib_channels.py index 2db8388fb..2f65666b7 100644 --- a/fractal_tasks_core/lib_channels.py +++ b/fractal_tasks_core/lib_channels.py @@ -34,13 +34,7 @@ class Window(BaseModel): """ - Custom class for Omero-channel window, related to OME-NGFF v0.4 - - See https://ngff.openmicroscopy.org/0.4/#omero-md. - Main difference from the specs: - - 1. ``min`` and ``max`` are optional, since we have custom logic to set - their values. + Custom class for Omero-channel window, based on OME-NGFF v0.4. """ min: Optional[int] @@ -58,16 +52,7 @@ class Window(BaseModel): class OmeroChannel(BaseModel): """ - Custom class for Omero channels, related to OME-NGFF v0.4. - - Differences from OME-NGFF v0.4 specs - (https://ngff.openmicroscopy.org/0.4/#omero-md): - - 1. Additional attributes ``wavelength_id`` and ``index``. - 2. We make ``color`` an optional attribute, since we have custom - logic to set its value. - 3. We make ``window`` an optional attribute, so that we can also - process zarr arrays which do not have this attribute. + Custom class for Omero channels, based on OME-NGFF v0.4. """ # Custom @@ -84,10 +69,10 @@ class OmeroChannel(BaseModel): """Channel label.""" window: Optional[Window] - """A ``Window`` object to display this channel in napari.""" + """Optional ``Window`` object to display this channel in napari.""" color: Optional[str] - """A colormap to display the channel in napari (e.g. ``00FFFF``).""" + """Optional colormap to display the channel in napari (e.g. ``00FFFF``).""" active: bool = True """Omero-channel attribute."""