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
25 changes: 25 additions & 0 deletions src/communitymech/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,31 @@ def default_record_roots() -> list[Path]:
return [KB_COMMUNITIES, DATA_ISOLATES]


def record_files() -> list[Path]:
"""Every `MicrobialCommunity` record file, across every root (#689).

`default_record_roots()` answers "which directories"; almost every caller
then writes the same `for root in ...: root.glob("*.yaml")` line. Eleven test
modules skipped that step entirely and globbed `kb/communities` alone --
correct-looking, and blind to 4 records carrying 66 snippets, 3 interactions
and 3 GTDB groundings.

One function so the loop is written once, and so a module that wants both
roots cannot get half of them by writing slightly less code than the module
next to it.
"""
return [path for root in default_record_roots() for path in sorted(root.glob("*.yaml"))]


def record_path(name: str) -> Path | None:
"""Resolve a record filename against every root, for named fixtures."""
for root in default_record_roots():
candidate = root / name
if candidate.is_file():
return candidate
return None


def taxon_descriptor_roots() -> list[Path]:
"""Every directory whose records can carry a `TaxonDescriptor` (#656).

Expand Down
8 changes: 7 additions & 1 deletion tests/test_community_level_connectivity_credit.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@
import pytest
import yaml

from communitymech.paths import record_files

REPO = pathlib.Path(__file__).parent.parent

# Both record roots, not kb/communities alone. `data/isolates` holds the same
# root class -- 4 records with 66 snippets, 3 ecological_interactions and 3
# gtdb_classification blocks -- and this module could not see any of it (#689).
COMMUNITIES = REPO / "kb/communities"


Expand All @@ -54,7 +60,7 @@ def _names(block: dict) -> set[str]:
def _survey() -> dict[str, int]:
records = with_cl = mixed = cl_only = 0
taxa = solely = 0
for path in sorted(COMMUNITIES.glob("*.yaml")):
for path in record_files():
document = yaml.safe_load(path.read_text()) or {}
records += 1
interactions = [
Expand Down
17 changes: 9 additions & 8 deletions tests/test_cultivation_units_are_constrained.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,14 @@ class attributes, which is why `test_the_generated_datamodel_carries_them`
import pytest
import yaml

from communitymech.paths import record_files

REPO = pathlib.Path(__file__).parent.parent
SCHEMA = REPO / "src/communitymech/schema/communitymech.yaml"
COMMUNITIES = REPO / "kb/communities"

# Both record roots, not kb/communities alone. `data/isolates` holds the same
# root class -- 4 records with 66 snippets, 3 ecological_interactions and 3
# gtdb_classification blocks -- and this module could not see any of it (#689).

# slot -> the enum it must be ranged to.
CONSTRAINED = {
Expand Down Expand Up @@ -80,7 +85,7 @@ def test_every_value_in_the_corpus_is_permissible(schema):
"""
enums = schema["enums"]
offenders = []
for path in sorted(COMMUNITIES.glob("*.yaml")):
for path in record_files():
for entry in (yaml.safe_load(path.read_text()) or {}).get("cultivation_setup") or []:
for slot, enum_name in CONSTRAINED.items():
value = entry.get(slot)
Expand All @@ -95,7 +100,7 @@ def test_the_corpus_actually_uses_these_slots():
"""Guard: at zero populated slots the test above passes on nothing."""
populated = sum(
1
for path in COMMUNITIES.glob("*.yaml")
for path in record_files()
for entry in (yaml.safe_load(path.read_text()) or {}).get("cultivation_setup") or []
for slot in CONSTRAINED
if entry.get(slot) is not None
Expand Down Expand Up @@ -127,11 +132,7 @@ def test_a_wrong_unit_is_actually_rejected(tmp_path):
real record, writes the exact string this issue was filed about, and
requires a non-zero exit.
"""
source = next(
p
for p in sorted(COMMUNITIES.glob("*.yaml"))
if "operating_temperature_unit: °C" in p.read_text()
)
source = next(p for p in record_files() if "operating_temperature_unit: °C" in p.read_text())
broken = tmp_path / "broken.yaml"
broken.write_text(
source.read_text().replace(
Expand Down
19 changes: 16 additions & 3 deletions tests/test_gtdb_near_tie_marker.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,28 @@
from __future__ import annotations

import importlib.util
import pathlib
import subprocess
from pathlib import Path

import pytest
import yaml

from communitymech.paths import taxon_descriptor_roots
from communitymech.taxon_blocks import iter_taxon_descriptors

REPO = Path(__file__).parent.parent


# A GTDB grounding can live on any `TaxonDescriptor`, and the schema hangs that
# class off two roots: MicrobialCommunity.taxonomy[].taxon_term and
# CommonTaxon.taxon_term. So this needs BOTH the wider directory list and the
# shared walker -- iterating `document["taxonomy"]` over kb/taxa finds nothing,
# because a CommonTaxon has no `taxonomy` key at all (#656, #689).
def _record_paths() -> list[pathlib.Path]:
return [p for root in taxon_descriptor_roots() for p in sorted(root.glob("*.yaml"))]


def _module():
spec = importlib.util.spec_from_file_location("_gtdb", REPO / "scripts/gtdb_ground.py")
module = importlib.util.module_from_spec(spec)
Expand Down Expand Up @@ -85,9 +98,9 @@ def test_the_bound_is_where_the_population_gap_is():


def _grounded():
for path in sorted((REPO / "kb/communities").glob("*.yaml")):
for entry in (yaml.safe_load(path.read_text()) or {}).get("taxonomy") or []:
block = (entry.get("taxon_term") or {}).get("gtdb_classification")
for path in _record_paths():
for descriptor in iter_taxon_descriptors(yaml.safe_load(path.read_text())):
block = descriptor.get("gtdb_classification")
if block:
yield path.name, block

Expand Down
19 changes: 11 additions & 8 deletions tests/test_gtdb_status_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import pytest
import yaml

from communitymech.paths import taxon_descriptor_roots
from communitymech.taxon_blocks import iter_taxon_descriptors

REPO = Path(__file__).parent.parent


Expand Down Expand Up @@ -435,14 +438,14 @@ def test_every_curated_pin_carries_a_note_and_a_value():
recorded, so a silent change to a pinned grounding fails.
"""
pins = {}
for path in sorted((REPO / "kb/communities").glob("*.yaml")):
for entry in yaml.safe_load(path.read_text()).get("taxonomy") or []:
term_block = entry.get("taxon_term") or {}
block = term_block.get("gtdb_classification") or {}
if block.get("curated"):
key = (path.name, (term_block.get("term") or {}).get("id"))
pins[key] = block.get("gtdb_id")
assert block.get("curation_note"), f"{path.name}: curated with no note"
for root in taxon_descriptor_roots():
for path in sorted(root.glob("*.yaml")):
for term_block in iter_taxon_descriptors(yaml.safe_load(path.read_text())):
block = term_block.get("gtdb_classification") or {}
if block.get("curated"):
key = (path.name, (term_block.get("term") or {}).get("id"))
pins[key] = block.get("gtdb_id")
assert block.get("curation_note"), f"{path.name}: curated with no note"

assert pins, "no curated pins found; the flag protects nothing"
# The two known pins are value-pinned, so a mapping build that makes either
Expand Down
14 changes: 9 additions & 5 deletions tests/test_interaction_participants_outside_taxonomy.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@
import pytest

from communitymech.network.auditor import IssueType, NetworkIntegrityAuditor
from communitymech.paths import record_files

REPO = pathlib.Path(__file__).parent.parent
COMMUNITIES = REPO / "kb/communities"

# Both record roots, not kb/communities alone. `data/isolates` holds the same
# root class -- 4 records with 66 snippets, 3 ecological_interactions and 3
# gtdb_classification blocks -- and this module could not see any of it (#689).

# (record, participant) for every interaction endpoint absent from `taxonomy`,
# grouped by why it is absent. Sourced from the auditor itself, not re-derived:
Expand Down Expand Up @@ -121,9 +125,9 @@

def _outside_taxonomy() -> set[tuple[str, str]]:
"""Every (record, participant) the auditor reports as not a member."""
auditor = NetworkIntegrityAuditor(COMMUNITIES)
auditor = NetworkIntegrityAuditor()
found = set()
for path in sorted(COMMUNITIES.glob("*.yaml")):
for path in record_files():
for issue in auditor.audit_community(path) or []:
if issue["type"] in (IssueType.UNKNOWN_SOURCE, IssueType.UNKNOWN_TARGET):
found.add((path.name, issue.get("taxon")))
Expand Down Expand Up @@ -181,9 +185,9 @@ def test_all_of_them_are_warnings_not_errors(outside):
interaction turns that participant into an error and reddens the build on a
record nobody changed the biology of.
"""
auditor = NetworkIntegrityAuditor(COMMUNITIES)
auditor = NetworkIntegrityAuditor()
errors = []
for path in sorted(COMMUNITIES.glob("*.yaml")):
for path in record_files():
for issue in auditor.audit_community(path) or []:
if (
issue["type"] in (IssueType.UNKNOWN_SOURCE, IssueType.UNKNOWN_TARGET)
Expand Down
18 changes: 14 additions & 4 deletions tests/test_ncbi_domain_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,22 @@
import pytest
import yaml

from communitymech.paths import taxon_descriptor_roots
from communitymech.taxon_blocks import iter_taxon_descriptors
from communitymech.validators.ncbi_domain import BACTERIA, EUKARYOTA, domain_of, outside_gtdb_scope

REPO = pathlib.Path(__file__).parent.parent


# A GTDB grounding can live on any `TaxonDescriptor`, and the schema hangs that
# class off two roots: MicrobialCommunity.taxonomy[].taxon_term and
# CommonTaxon.taxon_term. So this needs BOTH the wider directory list and the
# shared walker -- iterating `document["taxonomy"]` over kb/taxa finds nothing,
# because a CommonTaxon has no `taxonomy` key at all (#656, #689).
def _record_paths() -> list[pathlib.Path]:
return [p for root in taxon_descriptor_roots() for p in sorted(root.glob("*.yaml"))]


@pytest.mark.parametrize(
("curie", "expected"),
[
Expand Down Expand Up @@ -79,9 +90,8 @@ def test_an_unavailable_adapter_degrades_rather_than_guesses(monkeypatch):
def _statuses():
counts = collections.Counter()
offenders = []
for path in sorted((REPO / "kb/communities").glob("*.yaml")):
for entry in (yaml.safe_load(path.read_text()) or {}).get("taxonomy") or []:
term_block = entry.get("taxon_term") or {}
for path in _record_paths():
for term_block in iter_taxon_descriptors(yaml.safe_load(path.read_text())):
status = term_block.get("gtdb_grounding_status")
if not status:
continue
Expand Down Expand Up @@ -121,7 +131,7 @@ def test_sulcia_is_a_bacterium_not_a_spider():
The id↔label gate cannot catch this class: `NCBITaxon:2716471` really is
labelled "Sulcia" (#292).
"""
for path in sorted((REPO / "kb/communities").glob("*.yaml")):
for path in _record_paths():
text = path.read_text()
assert "NCBITaxon:2716471" not in text, f"{path.name} still uses the spider id"

Expand Down
15 changes: 11 additions & 4 deletions tests/test_network_auditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
issue_severity,
severity_of,
)
from communitymech.paths import record_files


@pytest.fixture
Expand Down Expand Up @@ -1268,10 +1269,16 @@ def test_the_kb_has_no_duplicate_taxon_names():
error — so the test audited zero records and passed vacuously (#334). It is
the sole support for gating on this finding, so an empty sweep must fail.
"""
communities = Path(__file__).parent.parent / "kb/communities"
assert len(list(communities.glob("*.yaml"))) > 100, "audited an empty or wrong directory"

auditor = NetworkIntegrityAuditor(communities_dir=communities)
# No `communities_dir=` override. Passing one silently replaced the
# auditor's own `default_record_roots()` default, so this swept
# kb/communities alone -- the shape #350 fixed IN the auditor and left
# standing in its test. The auditor's default is the thing under test here
# as much as the corpus is (#689).
records = record_files()
assert len(records) > 100, "audited an empty or wrong directory"
assert any(p.parent.name == "isolates" for p in records), "isolates are not being audited"

auditor = NetworkIntegrityAuditor()
auditor.audit_all(quiet=True)

offenders = sorted(
Expand Down
7 changes: 6 additions & 1 deletion tests/test_no_duplicate_yaml_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
import pytest
import yaml

from communitymech.paths import record_files

# Both record roots, not kb/communities alone. `data/isolates` holds the same
# root class -- 4 records with 66 snippets, 3 ecological_interactions and 3
# gtdb_classification blocks -- and this module could not see any of it (#689).
COMMUNITIES = Path(__file__).parent.parent / "kb/communities"

# Empty since #289 was fixed: both records that needed a curator decision have
Expand Down Expand Up @@ -58,7 +63,7 @@ def construct_mapping(loader, node, deep=False):


def _community_files() -> list[Path]:
return sorted(COMMUNITIES.glob("*.yaml"))
return record_files()


def test_there_are_community_files_to_check():
Expand Down
9 changes: 7 additions & 2 deletions tests/test_no_vacuous_go_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@
import pytest
import yaml

from communitymech.paths import record_files

REPO = pathlib.Path(__file__).parent.parent
COMMUNITIES = REPO / "kb/communities"

# Both record roots, not kb/communities alone. `data/isolates` holds the same
# root class -- 4 records with 66 snippets, 3 ecological_interactions and 3
# gtdb_classification blocks -- and this module could not see any of it (#689).

# GO terms so close to the root of the biological-process branch that asserting
# them of a microbial community conveys nothing. Each needs a reason, so that
Expand Down Expand Up @@ -66,7 +71,7 @@ def _walk(node, filename):

def _descriptors(corpus: pathlib.Path | None = None):
"""Every (file, preferred_term, id, label) biological-process descriptor."""
for path in sorted((corpus or COMMUNITIES).glob("*.yaml")):
for path in sorted(corpus.glob("*.yaml")) if corpus else record_files():
document = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
yield from _walk(document, path.name)

Expand Down
7 changes: 6 additions & 1 deletion tests/test_participating_taxa.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@
import yaml

from communitymech.network.auditor import IssueType, NetworkIntegrityAuditor
from communitymech.paths import record_files

REPO = pathlib.Path(__file__).parent.parent

# Both record roots, not kb/communities alone. `data/isolates` holds the same
# root class -- 4 records with 66 snippets, 3 ecological_interactions and 3
# gtdb_classification blocks -- and this module could not see any of it (#689).
COMMUNITIES = REPO / "kb/communities"
# #312's illustration: 28 taxa, every interaction COMMUNITY_LEVEL.
EXAMPLE = COMMUNITIES / "GLBRC_Populus_Variovorax_SynCom28.yaml"
Expand Down Expand Up @@ -157,7 +162,7 @@ def test_the_corpus_is_unchanged_by_this_feature():
"""
users = [
path.name
for path in sorted(COMMUNITIES.glob("*.yaml"))
for path in record_files()
for interaction in (yaml.safe_load(path.read_text()) or {}).get("ecological_interactions")
or []
if isinstance(interaction, dict) and interaction.get("participating_taxa")
Expand Down
Loading
Loading