diff --git a/bioimageio/spec/generic/_v0_3_converter.py b/bioimageio/spec/generic/_v0_3_converter.py index de095a297..803f052a3 100644 --- a/bioimageio/spec/generic/_v0_3_converter.py +++ b/bioimageio/spec/generic/_v0_3_converter.py @@ -1,4 +1,5 @@ import collections.abc +import string from .._internal.io import BioimageioYamlContent from ._v0_2_converter import convert_from_older_format as convert_from_older_format_v0_2 @@ -22,6 +23,12 @@ def convert_from_older_format(data: BioimageioYamlContent) -> None: _ = data.pop("download_url", None) _ = data.pop("rdf_source", None) + if "name" in data and isinstance(data["name"], str): + data["name"] = "".join( + c if c in string.ascii_letters + string.digits + "_- ()" else " " + for c in data["name"] + )[:128] + data["format_version"] = "0.3.0" diff --git a/bioimageio/spec/generic/v0_3.py b/bioimageio/spec/generic/v0_3.py index 4611bbb79..2ecc4b578 100644 --- a/bioimageio/spec/generic/v0_3.py +++ b/bioimageio/spec/generic/v0_3.py @@ -1,9 +1,10 @@ from __future__ import annotations +import string from functools import partial from typing import Any, Dict, List, Literal, Optional, Sequence, TypeVar, Union -from annotated_types import Len, LowerCase, MaxLen +from annotated_types import Len, LowerCase, MaxLen, MinLen from pydantic import Field, ValidationInfo, field_validator, model_validator from typing_extensions import Annotated @@ -37,9 +38,13 @@ from .._internal.types import RelativeFilePath as RelativeFilePath from .._internal.types import ResourceId as ResourceId from .._internal.url import HttpUrl as HttpUrl -from .._internal.validator_annotations import AfterValidator, Predicate +from .._internal.validator_annotations import ( + AfterValidator, + Predicate, + RestrictCharacters, +) from .._internal.version_type import Version as Version -from .._internal.warning_levels import ALERT +from .._internal.warning_levels import ALERT, INFO from ._v0_3_converter import convert_from_older_format from .v0_2 import VALID_COVER_IMAGE_EXTENSIONS, CoverImageSource from .v0_2 import Author as _Author_v0_2 @@ -158,8 +163,17 @@ class LinkedResource(Node): class GenericModelDescrBase(ResourceDescrBase): """Base for all resource descriptions including of model descriptions""" + name: Annotated[ + Annotated[ + str, RestrictCharacters(string.ascii_letters + string.digits + "_- ()") + ], + MinLen(5), + MaxLen(128), + warn(MaxLen(64), "Name longer than 64 characters.", INFO), + ] name: Annotated[NotEmpty[str], MaxLen(128)] - """A human-friendly name of the resource description""" + """A human-friendly name of the resource description. + May only contains letters, digits, underscore, minus, parentheses and spaces.""" description: Annotated[ str, MaxLen(1024), warn(MaxLen(512), "Description longer than 512 characters.") diff --git a/bioimageio/spec/model/v0_5.py b/bioimageio/spec/model/v0_5.py index 8e009bd6e..08505f734 100644 --- a/bioimageio/spec/model/v0_5.py +++ b/bioimageio/spec/model/v0_5.py @@ -2,6 +2,7 @@ import collections.abc import re +import string import warnings from abc import ABC from copy import deepcopy @@ -49,6 +50,7 @@ from typing_extensions import Annotated, LiteralString, Self, assert_never from bioimageio.spec._internal.validated_string import ValidatedString +from bioimageio.spec._internal.validator_annotations import RestrictCharacters from .._internal.common_nodes import ( Converter, @@ -1986,13 +1988,16 @@ def _validate_tensor_references_in_proc_kwargs(self, info: ValidationInfo) -> Se # def validate_inputs(self, input_tensors: Mapping[TensorId, NDArray[Any]]) -> Mapping[TensorId, NDArray[Any]]: name: Annotated[ - str, + Annotated[ + str, RestrictCharacters(string.ascii_letters + string.digits + "_- ()") + ], MinLen(5), + MaxLen(128), warn(MaxLen(64), "Name longer than 64 characters.", INFO), ] """A human-readable name of this model. It should be no longer than 64 characters - and may only contain letter, number, underscore, minus or space characters. + and may only contain letter, number, underscore, minus, parentheses and spaces. We recommend to chose a name that refers to the model's task and image modality. """ @@ -2221,6 +2226,11 @@ class _ModelConv(Converter[_ModelDescr_v0_4, ModelDescr]): def _convert( self, src: _ModelDescr_v0_4, tgt: "type[ModelDescr] | type[dict[str, Any]]" ) -> "ModelDescr | dict[str, Any]": + name = "".join( + c if c in string.ascii_letters + string.digits + "_- ()" else " " + for c in src.name + ) + def conv_authors(auths: Optional[Sequence[_Author_v0_4]]): conv = ( _author_conv.convert if TYPE_CHECKING else _author_conv.convert_as_dict @@ -2284,7 +2294,7 @@ def conv_authors(auths: Optional[Sequence[_Author_v0_4]]): maintainers=[ _maintainer_conv.convert_as_dict(m) for m in src.maintainers ], # pyright: ignore[reportArgumentType] - name=src.name, + name=name, tags=src.tags, type=src.type, uploader=src.uploader, diff --git a/tests/test_model/test_v0_5.py b/tests/test_model/test_v0_5.py index 616c6b9bc..a6e694ed3 100644 --- a/tests/test_model/test_v0_5.py +++ b/tests/test_model/test_v0_5.py @@ -275,7 +275,6 @@ def model_data(): @pytest.mark.parametrize( "update", [ - pytest.param(dict(name="ยต-unicode-model/name!"), id="unicode name"), dict(run_mode={"name": "special_run_mode", "kwargs": dict(marathon=True)}), dict( weights={ diff --git a/tests/test_specific_reexports_generics.py b/tests/test_specific_reexports_generics.py index a160c9e88..1a27ff103 100644 --- a/tests/test_specific_reexports_generics.py +++ b/tests/test_specific_reexports_generics.py @@ -29,6 +29,7 @@ "get_args", "ImportantFileSource", "include_in_package_serializer", + "INFO", "issue_warning", "Len", "LicenseId", @@ -38,6 +39,7 @@ "Mapping", "MarkdownSource", "MaxLen", + "MinLen", "model_validator", "Node", "NotEmpty", @@ -47,9 +49,11 @@ "requests", "ResourceDescrBase", "ResourceDescrType", + "RestrictCharacters", "Self", "Sequence", "settings", + "string", "TAG_CATEGORIES", "TypeVar", "Union",