Download tests/test_taxonomy.py from HuggingFaceBio/carbon-a-database-explorer: direct link, hf CLI and curl.
- Browser
- Download file 11.9 kB
-
https://huggingface.co/spaces/HuggingFaceBio/carbon-a-database-explorer/resolve/main/tests/test_taxonomy.py
- Command line
-
hf download hf://spaces/HuggingFaceBio/carbon-a-database-explorer/tests/test_taxonomy.py
-
curl -L -o test_taxonomy.py https://huggingface.co/spaces/HuggingFaceBio/carbon-a-database-explorer/resolve/main/tests/test_taxonomy.py
11.9 kB
| import json | |
| import re | |
| import sqlite3 | |
| import unittest | |
| from refresh_common_names import CURATED | |
| from taxonomy import COMMON_NAMES, DATABASE, DEFAULT_PATH, ICON_DIR, ICONS, MAX_ENGLISH, SHORTCUTS, Taxonomy | |
| class TaxonomyTests(unittest.TestCase): | |
| def setUpClass(cls): | |
| cls.taxonomy = Taxonomy() | |
| def levels(self, html): | |
| """Each level sits at its own y; bars are horizontal with a fixed height.""" | |
| return sorted({float(y) for y in re.findall(r'<rect x="[\d.]+" y="([\d.]+)" width="[\d.]+" height="15" class="bar-outline"', html)}) | |
| def test_expansion_keeps_every_ancestor_level(self): | |
| self.assertEqual(len(self.levels(self.taxonomy.view("2759"))), 2) | |
| self.assertEqual(len(self.levels(self.taxonomy.view(DEFAULT_PATH))), 3) | |
| html = self.taxonomy.view("2759/33154/33208") | |
| levels = self.levels(html) | |
| self.assertEqual(len(levels), 4) | |
| # Levels run down the page, evenly spaced and clear of each other, so | |
| # depth costs height rather than width. | |
| pitches = {round(b - a) for a, b in zip(levels, levels[1:])} | |
| self.assertEqual(len(pitches), 1, levels) | |
| caption_and_bar = 46 + 2 * 15 + 15 | |
| self.assertGreater(pitches.pop(), caption_and_bar) | |
| self.assertEqual(html.count('class="tree-node is-selected"'), 3) | |
| def test_search_path_expands_lineage(self): | |
| path = self.taxonomy.taxon_path(9606) | |
| self.assertTrue(path.startswith("2759/33154/33208/")) | |
| self.assertTrue(path.endswith("/9606")) | |
| self.assertIn(f'data-route="{path}"', self.taxonomy.view_taxon(9606)) | |
| def test_other_lineages_expand_as_columns(self): | |
| html = self.taxonomy.view("2759/2759~6") | |
| self.assertIn('data-path="2759/2759~6/2759~12"', html) | |
| def test_english_names_label_latin_clades(self): | |
| html = self.taxonomy.view("2759/33154/33208") | |
| for scientific, english in (("Opisthokonta", "animals and fungi"), ("Metazoa", "animals"), | |
| ("Eumetazoa", "animals with true tissues"), ("Porifera", "sponges")): | |
| self.assertIn(f'class="tree-label">{scientific}</text>', html) | |
| self.assertIn(f'class="tree-english">{english}</text>', html) | |
| # A gloss that only repeats the scientific name is dropped by the build. | |
| self.assertIn('class="tree-label">Fungi</text>', html) | |
| self.assertNotIn('class="tree-english">fungi</text>', html) | |
| def test_one_tooltip_per_node(self): | |
| html = self.taxonomy.view(DEFAULT_PATH) | |
| # <title> would render the browser's own tooltip on top of the styled one. | |
| self.assertNotIn("<title>", html) | |
| self.assertIn('data-detail="Viridiplantae (green plants) ·', html) | |
| def test_counts_live_in_the_tooltip_not_the_chart(self): | |
| html = self.taxonomy.view(DEFAULT_PATH) | |
| chart = html[html.index("<svg"):html.index("</svg>")] | |
| drawn = re.findall(r'<text[^>]*>([^<]*)</text>', chart) | |
| self.assertTrue(drawn) | |
| for text in drawn: | |
| self.assertNotRegex(text, r"\d,\d|%", f"counts belong in the tooltip, not the chart: {text}") | |
| self.assertIn("33,594 of 70,395 assemblies", html) | |
| def test_hovering_a_group_also_lights_its_inflowing_flux(self): | |
| html = self.taxonomy.view("2759/33154/33208") | |
| chart = html[html.index("<svg"):html.index("</svg>")] | |
| flows = dict(re.findall(r'<path d="[^"]*" class="flow flow-\w+" data-node="([\d-]+)" data-detail="([^"]*)"', chart)) | |
| groups = dict(re.findall(r'<g class="tree-node[^"]*"[^>]*data-node="([\d-]+)"[^>]*data-detail="([^"]*)"', chart)) | |
| self.assertTrue(flows) | |
| # Every ribbon carries the id of the group it arrives at, and the same text. | |
| for uid, detail in flows.items(): | |
| self.assertIn(uid, groups, uid) | |
| self.assertEqual(groups[uid], detail, uid) | |
| # The root has nothing flowing into it; every other level does. | |
| self.assertFalse(any(uid.startswith("0-") for uid in flows)) | |
| self.assertTrue(any(uid.startswith("1-") for uid in flows)) | |
| def test_curated_glosses_match_the_snapshot(self): | |
| with sqlite3.connect(f"{DATABASE.resolve().as_uri()}?mode=ro", uri=True) as conn: | |
| for taxid, (scientific, english) in CURATED.items(): | |
| row = conn.execute("SELECT name FROM taxa WHERE taxid=?", (taxid,)).fetchone() | |
| self.assertIsNotNone(row, f"taxid {taxid} is not in the snapshot") | |
| self.assertEqual(row[0], scientific, f"taxid {taxid}") | |
| self.assertLessEqual(len(english), MAX_ENGLISH, f"taxid {taxid} gloss is clipped in the chart") | |
| def test_published_names_cover_the_curated_table(self): | |
| names = json.loads(COMMON_NAMES.read_text())["names"] | |
| redundant = {t for t, (scientific, english) in CURATED.items() if scientific.casefold() == english.casefold()} | |
| self.assertEqual({int(t) for t in names} & set(CURATED), set(CURATED) - redundant) | |
| self.assertEqual(names["6040"], "sponges") # straight from NCBI Taxonomy | |
| def test_icons_are_free_to_use_and_present(self): | |
| if not ICONS.exists(): | |
| self.skipTest("silhouettes unavailable") | |
| payload = json.loads(ICONS.read_text()) | |
| self.assertTrue(payload["images"]) | |
| for image, meta in payload["images"].items(): | |
| # NonCommercial is legally murky for a company-run Space and ShareAlike | |
| # would reach into the repository; refresh_icons.py filters both out. | |
| self.assertNotIn("-nc", meta["license"], image) | |
| self.assertNotIn("-sa", meta["license"], image) | |
| self.assertTrue((ICON_DIR / f"{image}.svg").is_file(), image) | |
| for taxid, image in payload["taxa"].items(): | |
| self.assertIn(image, payload["images"], taxid) | |
| def test_small_groups_inherit_an_ancestor_icon(self): | |
| if not self.taxonomy.icons: | |
| self.skipTest("silhouettes unavailable") | |
| with sqlite3.connect(f"{DATABASE.resolve().as_uri()}?mode=ro", uri=True) as conn: | |
| conn.row_factory = sqlite3.Row | |
| own, borrowed = self.taxonomy.icon_for(conn, 2759) | |
| self.assertTrue(own) | |
| self.assertFalse(borrowed) | |
| # A taxon with no icon of its own still resolves, through its lineage. | |
| uncovered = next(r['taxid'] for r in conn.execute( | |
| "SELECT taxid FROM taxa WHERE total_count=1 ORDER BY taxid") | |
| if r['taxid'] in self.taxonomy.eukaryote_ids and r['taxid'] not in self.taxonomy.icons) | |
| image, borrowed = self.taxonomy.icon_for(conn, uncovered) | |
| self.assertTrue(image) | |
| self.assertTrue(borrowed) | |
| self.assertTrue((ICON_DIR / f"{image}.svg").is_file()) | |
| def test_borrowed_icons_never_repeat_within_a_column(self): | |
| if not self.taxonomy.icons: | |
| self.skipTest("silhouettes unavailable") | |
| for path in ("2759", DEFAULT_PATH, "2759/33154/33208", "2759/33154/33208/6072/33213"): | |
| html = self.taxonomy.view(path) | |
| # Levels are laid out top to bottom, so group the icons by their y. | |
| drawn = re.findall(r'<image class="tree-icon" x="[\d.]+" y="([\d.]+)"[^>]*?/data/icons/([0-9a-f-]+)\.svg"', html) | |
| levels = {} | |
| for y, image in drawn: | |
| levels.setdefault(round(float(y)), []).append(image) | |
| for y, images in levels.items(): | |
| self.assertEqual(len(images), len(set(images)), f"{path} level at {y}") | |
| def test_chart_references_icons_by_url(self): | |
| if not self.taxonomy.icons: | |
| self.skipTest("silhouettes unavailable") | |
| html = self.taxonomy.view(DEFAULT_PATH) | |
| self.assertIn('class="tree-icon"', html) | |
| for image in re.findall(r'href="[^"]*/data/icons/([0-9a-f-]+)\.svg"', html): | |
| self.assertIn(image, self.taxonomy.icon_credits) | |
| def test_viewbox_spans_the_whole_chart(self): | |
| # A stray local named width or height inside the node loop once shrank the | |
| # viewBox to 23x23, which renders as a blank chart with every element intact. | |
| for path in ("2759", DEFAULT_PATH, "2759/33154/33208"): | |
| box = re.search(r'<svg viewBox="0 0 ([\d.]+) ([\d.]+)"', self.taxonomy.view(path)) | |
| self.assertIsNotNone(box, path) | |
| self.assertGreater(float(box.group(1)), 1000, path) | |
| self.assertGreater(float(box.group(2)), 200, path) | |
| def test_shortcuts_expand_their_whole_lineage(self): | |
| html = self.taxonomy.view(DEFAULT_PATH) | |
| for taxid, name in SHORTCUTS: | |
| self.assertIn(f">{name}</button>", html) | |
| path = self.taxonomy.taxon_path(taxid) | |
| self.assertIn(f'data-path="{path}"', html) | |
| self.assertTrue(path.startswith("2759/"), name) | |
| self.assertTrue(path.endswith(f"/{taxid}"), name) | |
| def test_selected_path_flux_stays_highlighted(self): | |
| for path in (DEFAULT_PATH, "2759/33154/33208", "2759/33154/33208/6072/33213"): | |
| html = self.taxonomy.view(path) | |
| chart = html[html.index("<svg"):html.index("</svg>")] | |
| trunk = re.findall(r'class="flow flow-(\w+) is-path" data-node="([\d-]+)"', chart) | |
| selected = re.findall(r'<g class="tree-node is-selected"[^>]*data-node="([\d-]+)"', chart) | |
| # One level transition per step below the root, each carrying the | |
| # annotated and not-annotated halves of one group's inflow. | |
| self.assertEqual(sorted(set(uid for _, uid in trunk)), sorted(selected[1:]), path) | |
| self.assertLessEqual(len(trunk), 2 * (len(selected) - 1), path) | |
| # A trunk ribbon is still an ordinary ribbon, hoverable like any other. | |
| self.assertNotIn('class="flow flow-annotated is-path" data-node="0-0"', chart) | |
| def test_search_matches_english_names(self): | |
| for text, expected in (("sponges", "Porifera"), ("jellyfish", "Scyphozoa"), | |
| ("birds", "Aves"), ("human", "Homo sapiens")): | |
| found = self.taxonomy.search(text) | |
| self.assertTrue(found, text) | |
| # The closest match leads, so it can be preselected in the picker. | |
| self.assertTrue(found[0][0].startswith(expected + " · "), f"{text} -> {found[0][0]}") | |
| self.assertEqual(self.taxonomy.search("9606")[0][1], "9606") | |
| self.assertEqual(self.taxonomy.search("x"), []) | |
| def test_database_jump_offers_an_annotated_accession(self): | |
| with sqlite3.connect(f"{DATABASE.resolve().as_uri()}?mode=ro", uri=True) as conn: | |
| if not conn.execute("SELECT name FROM sqlite_master WHERE name='assemblies'").fetchone(): | |
| self.skipTest("assembly table unavailable") | |
| annotated = set(json.loads((DATABASE.parent / "coverage.json").read_text())["accessions"]) | |
| for path in (DEFAULT_PATH, "2759/33154/33208", self.taxonomy.taxon_path(40674)): | |
| found = self.taxonomy.assemblies_for(path, 3) | |
| self.assertTrue(found, path) | |
| # Every offer is a published annotation, never a dead end. | |
| for accession in found: | |
| self.assertIn(accession, annotated, path) | |
| # Mammal accessions must sit inside the mammal subtree, not above it. | |
| mammals = set(self.taxonomy.assemblies_for(self.taxonomy.taxon_path(40674), 50)) | |
| birds = set(self.taxonomy.assemblies_for(self.taxonomy.taxon_path(8782), 50)) | |
| self.assertFalse(mammals & birds) | |
| def test_rejects_paths_outside_the_tree(self): | |
| for path in ("1", "33208", "2759/33208", "2759/2759~3", "2759/9606", "abc"): | |
| with self.assertRaises(ValueError, msg=path): | |
| self.taxonomy.view(path) | |
| if __name__ == "__main__": | |
| unittest.main() | |