diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ab82c8..fb87a14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ between published tags; older pre-5.0 notes are retained below. For current interfaces, see the [consumer guide](docs/consumer-guide.md). +## 5.88.1 + +- Prefer the current predictor DataFrame API for cache fallbacks, retaining + compatibility with predictors that only implement the legacy API (#432). +- Require GTFParse's released Polars deprecation fix and mhctools' predictor + output handle cleanup. Close a test fixture's input file explicitly. +- Assert expected scientific diagnostics in tests and fail the suite on any + unexpected warning. + ## 5.88.0 - Import native Exacto peptide variants, primary structures and translations, diff --git a/pyproject.toml b/pyproject.toml index 3f38282..925ba5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ ignore = ["F821", "E501", "F841", "E731", "E741", "E722", "E721"] "test*/*" = ["F401"] [tool.pytest.ini_options] -filterwarnings = ["error::FutureWarning"] +filterwarnings = ["error"] markers = [ "isovar: exercises the installed Isovar optional integration", "pirlygenes: exercises the installed PirlyGenes optional integration", diff --git a/requirements.txt b/requirements.txt index 489b4b2..8071f11 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ numpy>=2.0.0,<3.0.0 pandas>=2.0.0 -mhctools>=3.44.26 +mhctools>=3.45.1 varcode>=4.18.0 -gtfparse>=2.7.0,<3.0.0 +gtfparse>=2.8.1,<3.0.0 mhcgnomes>=3.4.0 pyensembl>=2.3.0,<3.0.0 # PEP 440 versions and validated package names for release preflight diff --git a/tests/test_cached_predictor.py b/tests/test_cached_predictor.py index bba12e8..d95daa4 100644 --- a/tests/test_cached_predictor.py +++ b/tests/test_cached_predictor.py @@ -338,14 +338,36 @@ def _stamp(self, df): df["kind"] = "pMHC_affinity" return df - def predict_peptides_dataframe(self, peptides): - return self._stamp(super().predict_peptides_dataframe(peptides)) + def predict_dataframe(self, peptides, sample_name="", n_flanks=None, c_flanks=None): + return self._stamp(super().predict_dataframe( + peptides, sample_name=sample_name, n_flanks=n_flanks, c_flanks=c_flanks)) def predict_proteins_dataframe(self, name_to_seq): return self._stamp(super().predict_proteins_dataframe(name_to_seq)) class TestFallback: + def test_legacy_only_fallback_still_fills_misses(self): + from types import SimpleNamespace + + predictor = _matched_fallback(name="random", version="1.0") + fallback = SimpleNamespace( + predict_peptides_dataframe=predictor.predict_dataframe, + alleles=predictor.alleles, + ) + cache = CachedPredictor.from_dataframe(_df([_row()]), fallback=fallback) + assert cache.predict_dataframe(["GILGFVFTL"]).peptide.tolist() == ["GILGFVFTL"] + + def test_current_fallback_api_takes_precedence(self, monkeypatch): + fallback = _matched_fallback(name="random", version="1.0") + + def deprecated(*args, **kwargs): + pytest.fail("Cache called the deprecated fallback API") + + monkeypatch.setattr(fallback, "predict_peptides_dataframe", deprecated) + cache = CachedPredictor.from_dataframe(_df([_row()]), fallback=fallback) + assert cache.predict_dataframe(["GILGFVFTL"]).peptide.tolist() == ["GILGFVFTL"] + def test_fallback_fills_miss(self): cache = CachedPredictor.from_dataframe( _df([_row()]), diff --git a/tests/test_consumer_workflows.py b/tests/test_consumer_workflows.py index 3b17354..d915b94 100644 --- a/tests/test_consumer_workflows.py +++ b/tests/test_consumer_workflows.py @@ -2866,11 +2866,21 @@ def table(value, method, version): sources = {"only": table(50., "original", "")} else: sources = {"only": table(50., "original", None).drop(columns="predictor_version")} - combined = combine_sources(sources, sample_name="p") + if case == "versioned": + with pytest.warns(UserWarning) as diagnostics: + combined = combine_sources(sources, sample_name="p") + assert [str(warning.message) for warning in diagnostics] == [ + "Model 'original' has conflicting versions: '4.1b' vs '4.2'", + "Model 'original' has conflicting versions: '4.1b' vs ''", + ] + with pytest.warns(UserWarning, match="Multiple predictor versions"): + wide = combined.to_wide() + else: + combined = combine_sources(sources, sample_name="p") + wide = combined.to_wide() policy = dict(ascending=True, strata=["source_label"]) before = rank_candidates(combined, "affinity.value", **policy) for suffix, writer, method, reader in DELIMITED_IO_TWINS: - wide = combined.to_wide() path = tmp_path / f"combined.{suffix}" if writer_style == "result": method(wide, path) diff --git a/tests/test_io_pvacseq.py b/tests/test_io_pvacseq.py index 794022a..464473d 100644 --- a/tests/test_io_pvacseq.py +++ b/tests/test_io_pvacseq.py @@ -35,7 +35,8 @@ def _data_row_count(path): - return sum(1 for _ in open(path)) - 1 + with open(path) as handle: + return sum(1 for _ in handle) - 1 # --------------------------------------------------------------------------- diff --git a/tests/test_peptide_view.py b/tests/test_peptide_view.py index 24002fe..1946427 100644 --- a/tests/test_peptide_view.py +++ b/tests/test_peptide_view.py @@ -513,10 +513,11 @@ def test_default_methods_settles_the_dependence(): """An explicit default names one model; that model's mode applies.""" df = _two_model_presentation_df() - scores = evaluate_scores( - df, peptide_view(Presentation.score), kind_support=_SPLIT_SUPPORT, - default_methods={"pMHC_presentation": "netmhcpan"}, - ) + with pytest.warns(UserWarning, match="not a joint multi-allele aggregate"): + scores = evaluate_scores( + df, peptide_view(Presentation.score), kind_support=_SPLIT_SUPPORT, + default_methods={"pMHC_presentation": "netmhcpan"}, + ) # netmhcpan is single_allele: best of 0.9 / 0.4 across the peptide. assert scores.tolist()[:2] == [0.9, 0.9] @@ -526,10 +527,11 @@ def test_filter_auto_aggregation_binds_the_method_for_dependence(): """apply_filter binds one method per iteration; the resolver must see it.""" df = _two_model_presentation_df() - kept = apply_filter( - df, parse("peptide_view(el.score) >= 0.5"), - kind_support=_SPLIT_SUPPORT, - ) + with pytest.warns(UserWarning, match="not a joint multi-allele aggregate"): + kept = apply_filter( + df, parse("peptide_view(el.score) >= 0.5"), + kind_support=_SPLIT_SUPPORT, + ) assert len(kept) > 0 diff --git a/tests/test_ranking.py b/tests/test_ranking.py index c4f4d75..a421971 100644 --- a/tests/test_ranking.py +++ b/tests/test_ranking.py @@ -863,7 +863,8 @@ def test_default_methods_processing_synonyms(key): ]) from topiary.ranking import Processing ctx = EvalContext(df, default_methods={key: "mhcflurry"}) - assert Processing.score.eval(ctx).iloc[0] == 0.75 + with pytest.warns(UserWarning, match="peptide-level.*rows name alleles"): + assert Processing.score.eval(ctx).iloc[0] == 0.75 def test_default_methods_accepts_multiple_synonyms_in_one_dict(): @@ -3393,7 +3394,8 @@ def test_a_filter_on_an_unpopulated_reserved_scope_keeps_nothing(): from topiary import apply_filter df = _make_df(_UNSCOPED_ROW) - assert len(apply_filter(df, parse("shuffled.affinity < 500"))) == 0 + with pytest.warns(UserWarning, match="which this frame does not have"): + assert len(apply_filter(df, parse("shuffled.affinity < 500"))) == 0 # The same expression keeps the row once the producer supplies the column. supplied = _make_df([{**_UNSCOPED_ROW[0], "shuffled_value": 400.0}]) assert len(apply_filter(supplied, parse("shuffled.affinity < 500"))) == 1 diff --git a/topiary/__init__.py b/topiary/__init__.py index b80a3e3..99a3049 100644 --- a/topiary/__init__.py +++ b/topiary/__init__.py @@ -191,7 +191,7 @@ encode_amino_acids, ) -__version__ = "5.88.0" +__version__ = "5.88.1" __all__ = [ "normalize_isovar_rna_support", diff --git a/topiary/cached.py b/topiary/cached.py index 1ef13a9..42eea29 100644 --- a/topiary/cached.py +++ b/topiary/cached.py @@ -877,10 +877,10 @@ def _fallback_resolve(self, peptides) -> None: f"no fallback set. Missed peptides: {missed_preview}{extra}." ) - if hasattr(self.fallback, "predict_peptides_dataframe"): - fb_df = self.fallback.predict_peptides_dataframe(peptides) - elif hasattr(self.fallback, "predict_dataframe"): + if hasattr(self.fallback, "predict_dataframe"): fb_df = self.fallback.predict_dataframe(peptides) + elif hasattr(self.fallback, "predict_peptides_dataframe"): + fb_df = self.fallback.predict_peptides_dataframe(peptides) else: raise TypeError( f"fallback does not implement predict_peptides_dataframe "