Skip to content

Commit

Permalink
Merge pull request #18025 from jdavcs/24.0_tag_regex
Browse files Browse the repository at this point in the history
[24.0] Fix tag regex pattern
  • Loading branch information
jdavcs authored Apr 19, 2024
2 parents 9c26321 + ac8338f commit f98f6f7
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 4 deletions.
2 changes: 1 addition & 1 deletion client/src/components/Tags/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { keyedColorScheme } from "utils/color";

// Valid tag regex. The basic format here is a tag name with optional subtags
// separated by a period, and then an optional value after a colon.
export const VALID_TAG_RE = /^([^\s.:])+(.[^\s.:]+)*(:[^\s.:]+)?$/;
export const VALID_TAG_RE = /^([^\s.:])+(\.[^\s.:]+)*(:\S+)?$/;

export class TagModel {
/**
Expand Down
9 changes: 8 additions & 1 deletion lib/galaxy/model/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,14 @@ def get_tag_by_name(self, tag_name):
return None

def _create_tag(self, tag_str: str):
"""Create a Tag object from a tag string."""
"""
Create or retrieve one or more Tag objects from a tag string. If there are multiple
hierarchical tags in the tag string, the string will be split along `self.hierarchy_separator` chars.
A Tag instance will be created for each non-empty prefix. If a prefix corresponds to the
name of an existing tag, that tag will be retrieved; otherwise, a new Tag object will be created.
For example, for the tag string `a.b.c` 3 Tag instances will be created: `a`, `a.b`, `a.b.c`.
Return the last tag created (`a.b.c`).
"""
tag_hierarchy = tag_str.split(self.hierarchy_separator)
tag_prefix = ""
parent_tag = None
Expand Down
4 changes: 3 additions & 1 deletion lib/galaxy/schema/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@

OptionalNumberT = Annotated[Optional[Union[int, float]], Field(None)]

TAG_ITEM_PATTERN = r"^([^\s.:])+(\.[^\s.:]+)*(:\S+)?$"


class DatasetState(str, Enum):
NEW = "new"
Expand Down Expand Up @@ -527,7 +529,7 @@ class HistoryContentSource(str, Enum):
DatasetCollectionInstanceType = Literal["history", "library"]


TagItem = Annotated[str, Field(..., pattern=r"^([^\s.:])+(.[^\s.:]+)*(:[^\s.:]+)?$")]
TagItem = Annotated[str, Field(..., pattern=TAG_ITEM_PATTERN)]


class TagCollection(RootModel):
Expand Down
12 changes: 12 additions & 0 deletions test/unit/app/managers/test_TagHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,15 @@ def test_item_has_tag(self):
# Tag
assert self.tag_handler.item_has_tag(self.user, item=hda, tag=hda.tags[0].tag)
assert not self.tag_handler.item_has_tag(self.user, item=hda, tag="tag2")

def test_get_name_value_pair(self):
"""Verify that parsing a single tag string correctly splits it into name/value pairs."""
assert self.tag_handler.parse_tags("a") == [("a", None)]
assert self.tag_handler.parse_tags("a.b") == [("a.b", None)]
assert self.tag_handler.parse_tags("a.b:c") == [("a.b", "c")]
assert self.tag_handler.parse_tags("a.b:c.d") == [("a.b", "c.d")]
assert self.tag_handler.parse_tags("a.b:c.d:e.f") == [("a.b", "c.d:e.f")]
assert self.tag_handler.parse_tags("a.b:c.d:e.f.") == [("a.b", "c.d:e.f.")]
assert self.tag_handler.parse_tags("a.b:c.d:e.f..") == [("a.b", "c.d:e.f..")]
assert self.tag_handler.parse_tags("a.b:c.d:e.f:") == [("a.b", "c.d:e.f:")]
assert self.tag_handler.parse_tags("a.b:c.d:e.f::") == [("a.b", "c.d:e.f::")]
46 changes: 45 additions & 1 deletion test/unit/schema/test_schema.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import re
from uuid import uuid4

from pydantic import BaseModel

from galaxy.schema.schema import DatasetStateField
from galaxy.schema.schema import (
DatasetStateField,
TAG_ITEM_PATTERN,
)
from galaxy.schema.tasks import (
GenerateInvocationDownload,
RequestUser,
Expand Down Expand Up @@ -34,3 +38,43 @@ class StateModel(BaseModel):
def test_dataset_state_coercion():
assert StateModel(state="ok").state == "ok"
assert StateModel(state="deleted").state == "discarded"


class TestTagPattern:

def test_valid(self):
tag_strings = [
"a",
"aa",
"aa.aa",
"aa.aa.aa",
"~!@#$%^&*()_+`-=[]{};'\",./<>?",
"a.b:c",
"a.b:c.d:e.f",
"a.b:c.d:e..f",
"a.b:c.d:e.f:g",
"a.b:c.d:e.f::g",
"a.b:c.d:e.f::g:h",
"a::a", # leading colon for tag value
"a:.a", # leading period for tag value
"a:a:", # trailing colon OK for tag value
"a:a.", # trailing period OK for tag value
]
for t in tag_strings:
assert re.match(TAG_ITEM_PATTERN, t)

def test_invalid(self):
tag_strings = [
" a", # leading space for tag name
":a", # leading colon for tag name
".a", # leading period for tag name
"a ", # trailing space for tag name
"a a", # space inside tag name
"a: a", # leading space for tag value
"a:a a", # space inside tag value
"a:", # trailing colon for tag name
"a.", # trailing period for tag name
"a:b ", # trailing space for tag value
]
for t in tag_strings:
assert not re.match(TAG_ITEM_PATTERN, t)

0 comments on commit f98f6f7

Please sign in to comment.