Skip to content

Commit c06c06e

Browse files
committed
Update argument validation
1 parent 7521795 commit c06c06e

11 files changed

Lines changed: 83 additions & 20 deletions

File tree

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ minversion = 3.7
33
log_cli=true
44
python_files = test_*.py
55
;pytest_plugins = ['pytest_profiling']
6-
;addopts = -n 6 --dist loadscope
6+
addopts = -n 6 --dist loadscope

src/superannotate/lib/app/interface/sdk_interface.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2105,7 +2105,10 @@ def update_annotation_class(
21052105
21062106
"""
21072107
project = self.controller.get_project(project)
2108-
2108+
if project.type == ProjectType.MULTIMODAL:
2109+
raise AppException(
2110+
"This function is not supported for Multimodal projects."
2111+
)
21092112
# Find the annotation class by nam
21102113
annotation_classes = self.controller.annotation_classes.list(
21112114
condition=Condition("project_id", project.id, EQ)
@@ -3006,6 +3009,10 @@ def create_annotation_class(
30063009
except ValidationError as e:
30073010
raise AppException(wrap_error(e))
30083011
project = self.controller.get_project(project)
3012+
if project.type == ProjectType.MULTIMODAL:
3013+
raise AppException(
3014+
"This function is not supported for Multimodal projects."
3015+
)
30093016
if (
30103017
project.type != ProjectType.DOCUMENT
30113018
and annotation_class.type == ClassTypeEnum.RELATIONSHIP

src/superannotate/lib/core/entities/base.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pydantic import BaseModel
1212
from pydantic import ConfigDict
1313
from pydantic import Field
14+
from pydantic import PlainSerializer
1415
from pydantic_extra_types.color import Color
1516

1617
DATE_TIME_FORMAT_ERROR_MESSAGE = (
@@ -37,7 +38,20 @@ def _validate_string_date(v: datetime) -> str:
3738
return v.isoformat().split("+")[0] + ".000Z"
3839

3940

40-
StringDate = Annotated[datetime, AfterValidator(_validate_string_date)]
41+
def _serialize_string_date(v) -> str:
42+
"""Serialize datetime or string to string format."""
43+
if isinstance(v, str):
44+
return v
45+
if isinstance(v, datetime):
46+
return v.isoformat().split("+")[0] + ".000Z"
47+
return v
48+
49+
50+
StringDate = Annotated[
51+
datetime,
52+
AfterValidator(_validate_string_date),
53+
PlainSerializer(_serialize_string_date, return_type=str),
54+
]
4155

4256

4357
class SubSetEntity(BaseModel):

src/superannotate/lib/core/entities/work_managament.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ class WMAttributeGroup(TimedBaseModel):
205205
model_config = ConfigDict(extra="ignore")
206206

207207
id: Optional[StrictInt] = None
208-
group_type: Optional[WMGroupTypeEnum] = None
208+
group_type: WMGroupTypeEnum
209209
class_id: Optional[StrictInt] = None
210210
name: Optional[StrictStr] = None
211211
isRequired: bool = Field(default=False, alias="is_required")

src/superannotate/lib/infrastructure/validators.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,55 @@ def get_tabulation() -> int:
6565
return 48
6666

6767

68+
def _is_pydantic_internal_loc(loc_part: typing.Any) -> bool:
69+
"""
70+
Check if a location part is a Pydantic v2 internal type descriptor.
71+
These are not human-readable and should be filtered out.
72+
Examples of internal descriptors:
73+
- 'constrained-str'
74+
- 'lax-or-strict[...]'
75+
- 'list[SomeModel]'
76+
- 'json-or-python[...]'
77+
- 'function-after[...]'
78+
- 'union[...]'
79+
"""
80+
if not isinstance(loc_part, str):
81+
return False
82+
# These patterns indicate internal Pydantic type descriptors
83+
internal_patterns = (
84+
"constrained-",
85+
"lax-or-strict[",
86+
"json-or-python[",
87+
"function-after[",
88+
"function-before[",
89+
"function-wrap[",
90+
"union[",
91+
"is-instance[",
92+
)
93+
if loc_part.startswith(internal_patterns):
94+
return True
95+
# Also filter out patterns like 'list[Model]' or 'dict[str, Model]'
96+
# but keep simple field names
97+
if "[" in loc_part and "]" in loc_part:
98+
# Check if it looks like a type descriptor (e.g., 'list[Attachment]')
99+
# rather than a field name
100+
return True
101+
return False
102+
103+
68104
def wrap_error(e: ValidationError) -> str:
69105
tabulation = get_tabulation()
70106
error_messages = defaultdict(list)
71107
for error in e.errors():
72-
errors_list = (
73-
list(error["loc"])[:-1] if len(error["loc"]) > 1 else list(error["loc"])
74-
)
108+
error_loc = list(error["loc"])
109+
# Filter out Pydantic v2 internal type descriptors
110+
error_loc = [loc for loc in error_loc if not _is_pydantic_internal_loc(loc)]
111+
if len(error_loc) == 0:
112+
continue
113+
if len(error_loc) == 1 and isinstance(error_loc[0], int):
114+
errors_list = [f"argument at index {error_loc[0]}"]
115+
else:
116+
errors_list = list(error_loc)
75117
if "__root__" in errors_list:
76118
errors_list.remove("__root__")
77119
errors_list[1::] = [

tests/integration/folders/test_delete_folders.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def test_search_folders(self):
3333

3434
with self.assertRaisesRegex(AppException, "There is no folder to delete."):
3535
sa.delete_folders(self.PROJECT_NAME, [])
36-
pattern = r"(\s+)folder_names(\s+)Input should be a valid list"
36+
pattern = r"(\s+)argument at index 2(\s+)Input should be a valid list"
3737

3838
with self.assertRaisesRegex(AppException, pattern):
3939
sa.delete_folders(self.PROJECT_NAME, None) # noqa

tests/integration/items/test_attach_items.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,11 @@ class TestAttachItemsVectorArguments(TestCase):
117117
def test_attach_items_invalid_payload(self):
118118
error_msg = [
119119
"attachments",
120-
"str type expected",
121-
"value is not a valid path",
122-
r"attachments\[0].url",
123-
"field required",
120+
"Input should be a valid string",
121+
"Input is not a valid path",
122+
r"attachments\[0\]\.url",
123+
"Field required",
124124
]
125-
pattern = r"(\s+)" + r"(\s+)".join(error_msg)
125+
pattern = r"[\s\S]+" + r"[\s\S]+".join(error_msg)
126126
with self.assertRaisesRegex(AppException, pattern):
127-
sa.attach_items(self.PROJECT_NAME, [{"name": "name"}])
127+
sa.attach_items(self.PROJECT_NAME, attachments=[{"name": "name"}])

tests/integration/items/test_set_approval_statuses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def test_set_approval_statuses(self):
8181
def test_set_invalid_approval_statuses(self):
8282
sa.attach_items(self.PROJECT_NAME, [ATTACHMENT_LIST[0]])
8383
with self.assertRaisesRegex(
84-
AppException, "Available values are 'Approved', 'Disapproved'."
84+
AppException, "Input should be 'Approved', 'Disapproved' or None"
8585
):
8686
sa.set_approval_statuses(
8787
self.PROJECT_NAME,

tests/integration/projects/test_create_project.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def test_create_project_datetime(self):
9090
def test_create_project_with_wrong_type(self):
9191
with self.assertRaisesRegex(
9292
AppException,
93-
"Input should be 'Vector', 'Video', 'Document', 'Tiled', 'PointCloud', 'Multimodal'",
93+
"Input should be 'Vector', 'Video', 'Document', 'Tiled', 'PointCloud' or 'Multimodal'",
9494
):
9595
sa.create_project(self.PROJECT, "desc", "wrong_type")
9696

tests/integration/projects/test_set_project_status.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def test_set_project_status_fail(self, update_function):
5151
def test_set_project_status_via_invalid_status(self):
5252
with self.assertRaisesRegex(
5353
AppException,
54-
"Available values are 'NotStarted', 'InProgress', 'Completed', 'OnHold'.",
54+
"Input should be 'NotStarted', 'InProgress', 'Completed' or 'OnHold'",
5555
):
5656
sa.set_project_status(project=self.PROJECT_NAME, status="InvalidStatus")
5757

0 commit comments

Comments
 (0)