Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: LEAP-634: Actualize label config validation #5313

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
12 changes: 8 additions & 4 deletions label_studio/core/label_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@ def parse_config(config_string):

def _fix_choices(config):
"""
workaround for single choice
[ex] workaround for single choice
https://github.com/heartexlabs/label-studio/issues/1259

if you see the similar problem try to use another order of rules in the related part of the config inside `anyOf`
we do show only the first error so it might be meaningful
rules like `MaybeMultiple*` in that case work better when the option with single element comes before an array of elements
"""
if 'Choices' in config:
# for single Choices tag in View
Expand Down Expand Up @@ -84,7 +88,7 @@ def parse_config_to_json(config_string):
if xml is None:
raise etree.ParseError('xml is empty or incorrect')
config = xmljson.badgerfish.data(xml)
config = _fix_choices(config)
# previously we called `_fix_choices` here, but no longer need to because changing schema helps with that
return config


Expand All @@ -98,7 +102,7 @@ def validate_label_config(config_string):
except jsonschema.exceptions.ValidationError as exc:
error_message = exc.context[-1].message if len(exc.context) else exc.message
error_message = 'Validation failed on {}: {}'.format(
'/'.join(map(str, exc.path)), error_message.replace('@', '')
'/'.join(map(str, exc.absolute_path)), error_message.replace('@', '')
)
raise LabelStudioValidationErrorSentryIgnored(error_message)

Expand Down Expand Up @@ -291,7 +295,7 @@ def generate_sample_task_without_check(label_config, mode='upload', secure_mode=
for ts_child in p:
if ts_child.tag != 'Channel':
continue
value_columns.append(ts_child.get('column'))
value_columns.append(ts_child.get('column') or '_default_')
sep = p.get('sep')
time_format = p.get('timeFormat')

Expand Down
217 changes: 178 additions & 39 deletions label_studio/core/utils/schema/label_config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
"anyOf": [
{"type": "string"}
]},
"@column": {
"type": "string"
},
"tag_with_value": {
"type": "object",
"required": [
Expand Down Expand Up @@ -55,69 +58,164 @@
"@value": {
"$ref": "#/definitions/@value"
},
"@valueList": {
"$ref": "#/definitions/@valueList"
},
"$": {
"$ref": "#/definitions/$"
}
}
},
"tag_with_name_and_toname": {
"type": "object",
"oneOf": [
{
"required": [
"@name",
"@toName"
]
}
],
"properties": {
"@name": {
"$ref": "#/definitions/@name"
},
"@toName": {
"$ref": "#/definitions/@toName"
},
"$": {
"$ref": "#/definitions/$"
}
}
},
"tag_with_column": {
"type": "object",
"required": [
"@column"
],
"properties": {
"@column": {
"$ref": "#/definitions/@column"
},
"$": {
"$ref": "#/definitions/$"
}
}
},
"tags_with_value": {
"anyOf": [{
"anyOf": [{"$ref": "#/definitions/tag_with_value"},
{
"type": "array",
"items": {"$ref": "#/definitions/tag_with_value"}
}, {"$ref": "#/definitions/tag_with_value"}]
}]
},
"tags_with_value_or_object": {
"anyOf": [{
"type": "object"
}, {"$ref": "#/definitions/tag_with_value"},
{
"type": "array",
"items": {"$ref": "#/definitions/tag_with_value"}
}, {
"type": "object"
}, {"$ref": "#/definitions/tag_with_value"}]
}]
},
"tags_with_value_required_name": {
"anyOf": [{
"anyOf": [{"$ref": "#/definitions/tag_with_value_required_name"},
{
"type": "array",
"items": {"$ref": "#/definitions/tag_with_value_required_name"}
}, {"$ref": "#/definitions/tag_with_value_required_name"}]
}]
},
"tags_with_column": {
"anyOf": [{"$ref": "#/definitions/tag_with_column"},
{
"type": "array",
"items": {"$ref": "#/definitions/tag_with_column"}
}]
},
"tags_with_name_and_toname": {
"anyOf": [{"$ref": "#/definitions/tag_with_name_and_toname"},
{
"type": "array",
"items": {"$ref": "#/definitions/tag_with_name_and_toname"}
}]
},
"View": {
"type": "object",
"additionalProperties": true,
"properties": {
"Labels": {"$ref": "#/definitions/MaybeMultipleLabels"},
"Choices": {"$ref": "#/definitions/MaybeMultipleChoices"},
"Taxonomy": {"$ref": "#/definitions/MaybeMultipleChoices"},
"Label": {"$ref": "#/definitions/MaybeMultipleLabel"},
"Choice": {"$ref": "#/definitions/MaybeMultipleChoice"},
"Audio": {"$ref": "#/definitions/tags_with_value_required_name"},
"Image": {"$ref": "#/definitions/tags_with_value_required_name"},
"List": {"$ref": "#/definitions/tags_with_value_required_name"},
"Paragraphs": {"$ref": "#/definitions/tags_with_value_required_name"},
"Table": {"$ref": "#/definitions/tags_with_value"},
"Text": {"$ref": "#/definitions/tags_with_value_required_name"},
"TimeSeries": {"$ref": "#/definitions/MaybeMultipleTimeSeries"},
"HyperText": {"$ref": "#/definitions/tags_with_value_required_name"},
"View": {"$ref": "#/definitions/MaybeMultipleView"},
"TextArea": {"$ref": "#/definitions/MaybeMultipleTextAreas"}
"TextArea": {"$ref": "#/definitions/MaybeMultipleTextAreas"},
"Channel": {"$ref": "#/definitions/tags_with_column"},
"Video": {"$ref": "#/definitions/tags_with_value_required_name"},
"Brush": {"$ref": "#/definitions/tags_with_name_and_toname"},
"BrushLabels": {"$ref": "#/definitions/MaybeMultipleLabels"},
"DateTime": {"$ref": "#/definitions/tags_with_name_and_toname"},
"Ellipse": {"$ref": "#/definitions/tags_with_name_and_toname"},
"EllipseLabels": {"$ref": "#/definitions/MaybeMultipleLabels"},
"HyperTextLabels": {"$ref": "#/definitions/MaybeMultipleLabels"},
"KeyPoint": {"$ref": "#/definitions/tags_with_name_and_toname"},
"KeyPointLabels": {"$ref": "#/definitions/MaybeMultipleLabels"},
"MagicWand": {"$ref": "#/definitions/tags_with_name_and_toname"},
"Number": {"$ref": "#/definitions/tags_with_name_and_toname"},
"Pairwise": {"$ref": "#/definitions/tags_with_name_and_toname"},
"ParagraphLabels": {"$ref": "#/definitions/MaybeMultipleLabels"},
"Polygon": {"$ref": "#/definitions/tags_with_name_and_toname"},
"PolygonLabels": {"$ref": "#/definitions/MaybeMultipleLabels"},
"Ranker": {"$ref": "#/definitions/tags_with_name_and_toname"},
"Rating": {"$ref": "#/definitions/tags_with_name_and_toname"},
"Rectangle": {"$ref": "#/definitions/tags_with_name_and_toname"},
"RectangleLabels": {"$ref": "#/definitions/MaybeMultipleLabels"},
"Relations": {"$ref": "#/definitions/MaybeMultipleRelations"},
"TimeSeriesLabels": {"$ref": "#/definitions/MaybeMultipleLabels"},
"VideoRectangle": {"$ref": "#/definitions/tags_with_name_and_toname"}
}
},
"MaybeMultipleView": {
"anyOf": [{
"anyOf": [{"$ref": "#/definitions/View"},
{
"type": "array",
"items": {"$ref": "#/definitions/View"}
}, {"$ref": "#/definitions/View"}]
}]
},
"Choice": {
"$ref": "#/definitions/tag_with_value"
"allOf": [
{"$ref": "#/definitions/tag_with_value"},
{
"properties": {
"Choice": {"$ref": "#/definitions/MaybeMultipleChoice"}
}
}
]
},
"MaybeMultipleChoice": {
"anyOf": [{
"anyOf": [{"$ref": "#/definitions/Choice"},
{
"type": "array",
"items": {"$ref": "#/definitions/Choice"}
}, {"$ref": "#/definitions/Choice"}]
}]
},
"Label": {
"$ref": "#/definitions/tag_with_value"
},
"MaybeMultipleLabel": {
"anyOf": [{
"anyOf": [{"$ref": "#/definitions/Label"},
{
"type": "array",
"items": {"$ref": "#/definitions/Label"}
}, {"$ref": "#/definitions/Label"}]
}]
},
"Choices": {
"type": "object",
Expand All @@ -139,18 +237,16 @@
"$": {
"$ref": "#/definitions/$"
},
"Choice": {
"type": "array",
"items": {"$ref": "#/definitions/Choice"}
},
"Choice": {"$ref": "#/definitions/MaybeMultipleChoice"},
"View": {"$ref": "#/definitions/MaybeMultipleView"}
}
},
"MaybeMultipleChoices": {
"anyOf": [{
"anyOf": [{"$ref": "#/definitions/Choices"},
{
"type": "array",
"items": {"$ref": "#/definitions/Choices"}
}, {"$ref": "#/definitions/Choices"}]
}]
},
"Labels": {
"type": "object",
Expand All @@ -177,10 +273,27 @@
}
},
"MaybeMultipleLabels": {
"anyOf": [{
"anyOf": [{"$ref": "#/definitions/Labels"},
{
"type": "array",
"items": {"$ref": "#/definitions/Labels"}
}, {"$ref": "#/definitions/Labels"}]
}]
},
"Relations": {
"type": "object",
"properties": {
"$": {
"$ref": "#/definitions/$"
},
"Relation": {"$ref": "#/definitions/tags_with_value"}
}
},
"MaybeMultipleRelations": {
"anyOf": [{"$ref": "#/definitions/Relations"},
{
"type": "array",
"items": {"$ref": "#/definitions/Relations"}
}]
},
"TextArea": {
"type": "object",
Expand All @@ -196,31 +309,57 @@
},
"$": {
"$ref": "#/definitions/$"
}
},
"Shortcut": {"$ref": "#/definitions/tags_with_value"}
}
},
"MaybeMultipleTextAreas": {
"anyOf": [{
"anyOf": [ {"$ref": "#/definitions/TextArea"},
{
"type": "array",
"items": {"$ref": "#/definitions/TextArea"}
}, {"$ref": "#/definitions/TextArea"}]
}

},
"type": "object",
"properties": {
"View": {
}]
},
"TimeSeries": {
"type": "object",
"additionalProperties": true,
"oneOf": [
{
"required": [
"@name",
"@value"
]
},
{
"required": [
"@name",
"@valueList"
]
}
],
"properties": {
"Choices": {"$ref": "#/definitions/MaybeMultipleChoices"},
"Labels": {"$ref": "#/definitions/MaybeMultipleLabels"},
"@value": {
"$ref": "#/definitions/@value"
},
"@valueList": {
"$ref": "#/definitions/@valueList"
},
"$": {
"$ref": "#/definitions/$"
},
"View": {"$ref": "#/definitions/MaybeMultipleView"},
"Image": {"$ref": "#/definitions/tags_with_value_required_name"},
"Text": {"$ref": "#/definitions/tags_with_value_required_name"},
"HyperText": {"$ref": "#/definitions/tags_with_value_required_name"},
"TextArea": {"$ref": "#/definitions/MaybeMultipleTextAreas"}
"Channel": {"$ref": "#/definitions/tags_with_column"}
}
},
"MaybeMultipleTimeSeries": {
"anyOf": [{"$ref": "#/definitions/TimeSeries"},
{
"type": "array",
"items": {"$ref": "#/definitions/TimeSeries"}
}]
}
},
"type": "object",
"properties": {
"View": {"$ref": "#/definitions/View"}
}
}
4 changes: 2 additions & 2 deletions label_studio/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def any_api_client(request, client_and_token, business_client):
'title': '111',
'label_config': '<View><Text name="my_text" value="$text"/><Choices name="my_class"><Choice value="pos"/><Choice value="neg"/></Choices></View>',
},
{'label_config': ["Validation failed on : 'toName' is a required property"]},
{'label_config': ["Validation failed on View/Choices: 'toName' is a required property"]},
400,
),
# empty label config
Expand Down Expand Up @@ -119,7 +119,7 @@ def test_create_project(client_and_token, payload, response, status_code):
{
'label_config': '<View><Text name="my_text" value="$text"/><Choices name="my_class"><Choice value="pos"/><Choice value="neg"/></Choices></View>'
},
{'label_config': ["Validation failed on : 'toName' is a required property"]},
{'label_config': ["Validation failed on View/Choices: 'toName' is a required property"]},
400,
),
],
Expand Down
Loading