Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/test/term_info_queries_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,28 @@ def get_term_info_or_skip(self, term_id):
except Exception as e:
self.skipTest(f"SOLR server not available: {e}")

def test_label_stray_backslash_cleaned(self):
# Upstream Cypher / string-literal escaping can leave a stray backslash
# before an apostrophe in a label (e.g. "y5β\\'2a"). It must be
# stripped at ingestion so Name/Meta/links never carry it -- a backslash
# surviving into a downstream request target triggers a Tomcat
# "Invalid character found in the request target" 400.
terminfo_json = r'''
{"term": {"core": {"iri": "http://n2o.neo/individual/VFB_jrchk0e3",
"symbol": "", "types": ["Entity", "Individual", "Neuron"],
"short_form": "VFB_jrchk0e3", "unique_facets": ["Adult"],
"label": "MBON01(y5β\\'2a)_L"}, "description": [], "comment": []},
"query": "Get JSON for Individual", "version": "t", "parents": [],
"relationships": [], "xrefs": [], "anatomy_channel_image": [],
"pub_syn": [], "def_pubs": []}
'''
terminfo = deserialize_term_info(terminfo_json)
# backslash stripped, apostrophe and Greek beta preserved
self.assertEqual("MBON01(y5β'2a)_L", terminfo.term.core.label)
self.assertNotIn("\\", terminfo.term.core.label)
# and it flows clean into the internal link (used for Name/Meta rendering)
self.assertNotIn("\\", terminfo.term.core.get_int_link())

def test_term_info_deserialization(self):
terminfo_json = """
{"term": {"core": {"iri": "http://purl.obolibrary.org/obo/FBbt_00048514", "symbol": "BM-Taste", "types": ["Entity", "Adult", "Anatomy", "Cell", "Class", "Mechanosensory_system", "Nervous_system", "Neuron", "Sensory_neuron"], "short_form": "FBbt_00048514", "unique_facets": ["Adult", "Mechanosensory_system", "Nervous_system", "Sensory_neuron"], "label": "labial taste bristle mechanosensory neuron"}, "description": ["Any mechanosensory neuron (FBbt:00005919) that has sensory dendrite in some labellar taste bristle (FBbt:00004162)."], "comment": []}, "query": "Get JSON for Neuron Class", "version": "3d2a474", "parents": [{"symbol": "", "iri": "http://purl.obolibrary.org/obo/FBbt_00048508", "types": ["Entity", "Anatomy", "Cell", "Class", "Mechanosensory_system", "Nervous_system", "Neuron", "Sensory_neuron"], "short_form": "FBbt_00048508", "unique_facets": ["Mechanosensory_system", "Nervous_system", "Sensory_neuron"], "label": "mechanosensory neuron of chaeta"}, {"symbol": "", "iri": "http://purl.obolibrary.org/obo/FBbt_00051420", "types": ["Entity", "Adult", "Anatomy", "Cell", "Class", "Mechanosensory_system", "Nervous_system", "Neuron", "Sensory_neuron"], "short_form": "FBbt_00051420", "unique_facets": ["Adult", "Mechanosensory_system", "Nervous_system", "Sensory_neuron"], "label": "adult mechanosensory neuron"}, {"symbol": "", "iri": "http://purl.obolibrary.org/obo/FBbt_00048029", "types": ["Entity", "Adult", "Anatomy", "Cell", "Class", "Nervous_system", "Neuron", "Sensory_neuron"], "short_form": "FBbt_00048029", "unique_facets": ["Adult", "Nervous_system", "Sensory_neuron"], "label": "labellar taste bristle sensory neuron"}], "relationships": [{"relation": {"iri": "http://purl.obolibrary.org/obo/BFO_0000050", "label": "is part of", "type": "part_of"}, "object": {"symbol": "", "iri": "http://purl.obolibrary.org/obo/FBbt_00005892", "types": ["Entity", "Adult", "Anatomy", "Class", "Nervous_system"], "short_form": "FBbt_00005892", "unique_facets": ["Adult", "Nervous_system"], "label": "adult peripheral nervous system"}}], "xrefs": [], "anatomy_channel_image": [], "pub_syn": [{"synonym": {"scope": "has_exact_synonym", "label": "labellar taste bristle mechanosensitive neuron", "type": ""}, "pub": {"core": {"symbol": "", "iri": "http://flybase.org/reports/Unattributed", "types": ["Entity", "Individual", "pub"], "short_form": "Unattributed", "unique_facets": ["pub"], "label": ""}, "FlyBase": "", "PubMed": "", "DOI": ""}}, {"synonym": {"scope": "has_exact_synonym", "label": "labellar taste bristle mechanosensitive neuron", "type": ""}, "pub": {"core": {"symbol": "", "iri": "http://flybase.org/reports/Unattributed", "types": ["Entity", "Individual", "pub"], "short_form": "Unattributed", "unique_facets": ["pub"], "label": ""}, "FlyBase": "", "PubMed": "", "DOI": ""}}, {"synonym": {"scope": "has_exact_synonym", "label": "labial taste bristle mechanosensitive neuron", "type": ""}, "pub": {"core": {"symbol": "", "iri": "http://flybase.org/reports/Unattributed", "types": ["Entity", "Individual", "pub"], "short_form": "Unattributed", "unique_facets": ["pub"], "label": ""}, "FlyBase": "", "PubMed": "", "DOI": ""}}], "def_pubs": [{"core": {"symbol": "", "iri": "http://flybase.org/reports/FBrf0242472", "types": ["Entity", "Individual", "pub"], "short_form": "FBrf0242472", "unique_facets": ["pub"], "label": "Zhou et al., 2019, Sci. Adv. 5(5): eaaw5141"}, "FlyBase": "", "PubMed": "31131327", "DOI": "10.1126/sciadv.aaw5141"}], "targeting_splits": []}
Expand Down
26 changes: 25 additions & 1 deletion src/vfbquery/term_info_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ def default(self, obj):
from dacite import from_dict


# Stray backslash-escaping of quotes/apostrophes leaks in from upstream
# Cypher / string-literal escaping (e.g. a label stored as "y5β\\'2a"
# arrives as y5β\'2a -- a literal backslash before the apostrophe).
# A backslash before a quote/apostrophe is never legitimate in a display
# label, and if it survives into a downstream request target it triggers
# Tomcat "Invalid character found in the request target" 400s. Strip it at
# ingestion so every consumer (Name, Meta, links, synonyms) sees the clean
# value. Idempotent and safe to call on already-clean text.
_STRAY_QUOTE_ESCAPE = re.compile(r"\\(['\"])")


def clean_label(text):
if not isinstance(text, str) or "\\" not in text:
return text
return _STRAY_QUOTE_ESCAPE.sub(r"\1", text)


@dataclass_json
@dataclass
class Coordinates:
Expand Down Expand Up @@ -67,9 +84,16 @@ class MinimalEntityInfo:
unique_facets: Optional[List[str]] = None
symbol: Optional[str] = ""

def __post_init__(self):
# Clean stray escaping at ingestion so Name/Meta/links/symbol are all
# consistent (the top-level termInfo["Name"] reads self.label/self.symbol
# directly and previously kept the backslash).
self.label = clean_label(self.label)
self.symbol = clean_label(self.symbol)

def get_int_link(self, show_types=False) -> str:
if self.label:
result = get_link(self.label.replace("\\'", "'"), self.short_form) + " " + self.get_types_str(show_types)
result = get_link(self.label, self.short_form) + " " + self.get_types_str(show_types)
else:
result = get_link(self.short_form, self.short_form) + " " + self.get_types_str(show_types)
return result.strip()
Expand Down
Loading