diff --git a/mhctools/__init__.py b/mhctools/__init__.py index eea4659..2bb7785 100644 --- a/mhctools/__init__.py +++ b/mhctools/__init__.py @@ -88,7 +88,7 @@ def __getattr__(name): raise AttributeError( "module %r has no attribute %r" % (__name__, name)) -__version__ = "3.31.6" +__version__ = "3.31.7" __all__ = [ "Prediction", diff --git a/mhctools/base_commandline_predictor.py b/mhctools/base_commandline_predictor.py index c21db56..1de4b77 100644 --- a/mhctools/base_commandline_predictor.py +++ b/mhctools/base_commandline_predictor.py @@ -834,6 +834,18 @@ def predict_pairs_dataframe( return pd.DataFrame(columns=COLUMNS) return pd.concat(dfs, ignore_index=True) + def _require_alleles(self): + """Guard the allele-consuming predict paths. alleles is optional at + construction so a predictor can score explicit (peptide, allele) pairs + via predict_pairs(); but predict()/predict_peptides() score against + self.alleles, and with none configured they would silently return + nothing (see _allele_groups). Fail loud instead.""" + if not self.alleles: + raise ValueError( + "%s was constructed without alleles; pass alleles=[...] to the " + "constructor, or use predict_pairs(peptides, alleles) to score " + "explicit (peptide, allele) pairs." % type(self).__name__) + def predict(self, peptides, n_flanks=None, c_flanks=None): """ Predict for a list of peptide sequences. @@ -842,6 +854,7 @@ def predict(self, peptides, n_flanks=None, c_flanks=None): available, parses directly to Pred objects. Otherwise falls back to converting from BindingPrediction. """ + self._require_alleles() peptides, _, _ = self._check_flank_inputs( peptides, n_flanks, c_flanks) return self._predict_for_alleles( @@ -850,6 +863,7 @@ def predict(self, peptides, n_flanks=None, c_flanks=None): allele_cli_names=getattr(self, "_allele_cli_names", None)) def predict_peptides(self, peptides): + self._require_alleles() return self._predict_binding_predictions_for_alleles( peptides=peptides, alleles=self.alleles, diff --git a/mhctools/base_predictor.py b/mhctools/base_predictor.py index a197097..9ce97d9 100644 --- a/mhctools/base_predictor.py +++ b/mhctools/base_predictor.py @@ -112,7 +112,7 @@ class BasePredictor(object): def __init__( self, - alleles, + alleles=None, valid_alleles=None, default_peptide_lengths=None, min_peptide_length=8, @@ -154,6 +154,11 @@ def __init__( against the tool's own supported-allele list, so that non-human alleles like H-2-Qa1 or BoLA-amani.1 can still be requested). """ + # alleles is optional: a predictor built without one can still score + # explicit (peptide, allele) pairs via predict_pairs(). The + # allele-consuming predict()/predict_peptides() path guards on empty. + if alleles is None: + alleles = [] # I find myself often constructing a predictor with just one allele # so as a convenience, allow user to not wrap that allele as a list if type(alleles) is str: diff --git a/mhctools/netmhc.py b/mhctools/netmhc.py index df77a33..e96b656 100644 --- a/mhctools/netmhc.py +++ b/mhctools/netmhc.py @@ -16,7 +16,7 @@ from .netmhc3 import NetMHC3 from .netmhc4 import NetMHC4 -def NetMHC(alleles, +def NetMHC(alleles=None, default_peptide_lengths=[9], program_name="netMHC"): """ diff --git a/mhctools/netmhc3.py b/mhctools/netmhc3.py index 6e3c90c..98cc04d 100644 --- a/mhctools/netmhc3.py +++ b/mhctools/netmhc3.py @@ -16,7 +16,7 @@ class NetMHC3(BaseCommandlinePredictor): def __init__( self, - alleles, + alleles=None, program_name="netMHC", default_peptide_lengths=[9]): BaseCommandlinePredictor.__init__( diff --git a/mhctools/netmhc4.py b/mhctools/netmhc4.py index 966a9af..021e46c 100644 --- a/mhctools/netmhc4.py +++ b/mhctools/netmhc4.py @@ -16,7 +16,7 @@ class NetMHC4(BaseCommandlinePredictor): def __init__( self, - alleles, + alleles=None, program_name="netMHC", process_limit=0, default_peptide_lengths=[9]): diff --git a/mhctools/netmhc_cons.py b/mhctools/netmhc_cons.py index 1b288ca..fdf6d5b 100644 --- a/mhctools/netmhc_cons.py +++ b/mhctools/netmhc_cons.py @@ -16,7 +16,7 @@ class NetMHCcons(BaseCommandlinePredictor): def __init__( self, - alleles, + alleles=None, program_name="netMHCcons", process_limit=0, default_peptide_lengths=[9]): diff --git a/mhctools/netmhc_pan.py b/mhctools/netmhc_pan.py index 4dcfb14..73a00b8 100644 --- a/mhctools/netmhc_pan.py +++ b/mhctools/netmhc_pan.py @@ -47,7 +47,7 @@ def _parse_version(version_str): def NetMHCpan( - alleles, + alleles=None, program_name="netMHCpan", process_limit=-1, default_peptide_lengths=[9], diff --git a/mhctools/netmhc_pan28.py b/mhctools/netmhc_pan28.py index effe272..677162f 100644 --- a/mhctools/netmhc_pan28.py +++ b/mhctools/netmhc_pan28.py @@ -16,7 +16,7 @@ class NetMHCpan28(BaseCommandlinePredictor): def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[9], program_name="netMHCpan", process_limit=-1, diff --git a/mhctools/netmhc_pan3.py b/mhctools/netmhc_pan3.py index 1276f45..f195535 100644 --- a/mhctools/netmhc_pan3.py +++ b/mhctools/netmhc_pan3.py @@ -17,7 +17,7 @@ class NetMHCpan3(BaseCommandlinePredictor): def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[9], program_name="netMHCpan", process_limit=-1, diff --git a/mhctools/netmhc_pan4.py b/mhctools/netmhc_pan4.py index 27f2961..fc7dc1d 100644 --- a/mhctools/netmhc_pan4.py +++ b/mhctools/netmhc_pan4.py @@ -20,7 +20,7 @@ class NetMHCpan4(BaseCommandlinePredictor): def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[9], program_name="netMHCpan", process_limit=-1, @@ -78,7 +78,7 @@ class NetMHCpan4_EL(NetMHCpan4): """ def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[9], program_name="netMHCpan", process_limit=-1, @@ -98,7 +98,7 @@ class NetMHCpan4_BA(NetMHCpan4): """ def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[9], program_name="netMHCpan", process_limit=-1, diff --git a/mhctools/netmhc_pan41.py b/mhctools/netmhc_pan41.py index d232b22..2c540a8 100644 --- a/mhctools/netmhc_pan41.py +++ b/mhctools/netmhc_pan41.py @@ -20,7 +20,7 @@ class NetMHCpan41(BaseCommandlinePredictor): def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[9], program_name="netMHCpan", process_limit=-1, @@ -80,7 +80,7 @@ class NetMHCpan41_EL(NetMHCpan41): """ def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[9], program_name="netMHCpan", process_limit=-1, @@ -101,7 +101,7 @@ class NetMHCpan41_BA(NetMHCpan41): """ def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[9], program_name="netMHCpan", process_limit=-1, diff --git a/mhctools/netmhc_pan42.py b/mhctools/netmhc_pan42.py index 1b6bd2e..3e5e06b 100644 --- a/mhctools/netmhc_pan42.py +++ b/mhctools/netmhc_pan42.py @@ -20,7 +20,7 @@ class NetMHCpan42(BaseCommandlinePredictor): def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[9], program_name="netMHCpan", process_limit=-1, @@ -79,7 +79,7 @@ class NetMHCpan42_EL(NetMHCpan42): """NetMHCpan 4.2 in elution score mode.""" def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[9], program_name="netMHCpan", process_limit=-1, @@ -98,7 +98,7 @@ class NetMHCpan42_BA(NetMHCpan42): """NetMHCpan 4.2 in binding affinity mode.""" def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[9], program_name="netMHCpan", process_limit=-1, diff --git a/mhctools/netmhcii_pan.py b/mhctools/netmhcii_pan.py index fb0c011..805b029 100644 --- a/mhctools/netmhcii_pan.py +++ b/mhctools/netmhcii_pan.py @@ -105,7 +105,7 @@ class NetMHCIIpan3(NetMHCIIpanBase): """ def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[15, 16, 17, 18, 19, 20], program_name="netMHCIIpan", process_limit=-1, @@ -126,7 +126,7 @@ class NetMHCIIpan4(NetMHCIIpanBase): """ def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[15, 16, 17, 18, 19, 20], program_name="netMHCIIpan", process_limit=-1, @@ -161,7 +161,7 @@ class NetMHCIIpan4_EL(NetMHCIIpan4): """ def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[15, 16, 17, 18, 19, 20], program_name="netMHCIIpan", process_limit=-1, @@ -182,7 +182,7 @@ class NetMHCIIpan4_BA(NetMHCIIpan4): """ def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[15, 16, 17, 18, 19, 20], program_name="netMHCIIpan", process_limit=-1, @@ -198,7 +198,7 @@ def __init__( def NetMHCIIpan( - alleles, + alleles=None, process_limit=-1, program_name="netMHCIIpan", default_peptide_lengths=[15, 16, 17, 18, 19, 20], @@ -236,7 +236,7 @@ class NetMHCIIpan43(NetMHCIIpanBase): """ def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[15, 16, 17, 18, 19, 20], program_name="netMHCIIpan", process_limit=-1, @@ -270,7 +270,7 @@ class NetMHCIIpan43_EL(NetMHCIIpan43): """ def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[15, 16, 17, 18, 19, 20], program_name="netMHCIIpan", process_limit=-1, @@ -291,7 +291,7 @@ class NetMHCIIpan43_BA(NetMHCIIpan43): """ def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[15, 16, 17, 18, 19, 20], program_name="netMHCIIpan", process_limit=-1, diff --git a/mhctools/netmhcstabpan.py b/mhctools/netmhcstabpan.py index 0892659..e2c155b 100644 --- a/mhctools/netmhcstabpan.py +++ b/mhctools/netmhcstabpan.py @@ -17,7 +17,7 @@ class NetMHCstabpan(BaseCommandlinePredictor): def __init__( self, - alleles, + alleles=None, default_peptide_lengths=[], program_name="netMHCstabpan", process_limit=-1, diff --git a/tests/test_commandline_predict_pairs.py b/tests/test_commandline_predict_pairs.py index 3adf9ab..1656cdf 100644 --- a/tests/test_commandline_predict_pairs.py +++ b/tests/test_commandline_predict_pairs.py @@ -226,3 +226,36 @@ def test_predict_pairs_dataframe_skip_unsupported_all_unsupported_is_empty(): assert list(df.columns) == list(COLUMNS) assert df.empty + + +# ---- alleles optional at construction --------------------------------------- + +def test_base_predictor_alleles_optional_normalizes_to_empty(): + from mhctools.base_predictor import BasePredictor + + assert BasePredictor(default_peptide_lengths=[9]).alleles == [] + assert BasePredictor(alleles=None, default_peptide_lengths=[9]).alleles == [] + # an allele passed as a bare string still works + assert BasePredictor( + alleles="HLA-A*02:01", default_peptide_lengths=[9]).alleles == ["HLA-A*02:01"] + + +def test_predict_pairs_works_without_constructor_alleles(): + predictor = _PairStubPredictor() + predictor.alleles = [] # built for the pairs workflow, no default alleles + + results = predictor.predict_pairs( + ["SIINFEKLL", "GILGFVFTL"], ["HLA-A*02:01", "HLA-B*07:02"]) + + assert [r.preds[0].peptide for r in results] == ["SIINFEKLL", "GILGFVFTL"] + + +def test_predict_without_alleles_raises_rather_than_returning_nothing(): + predictor = _PairStubPredictor() + predictor.alleles = [] + + with pytest.raises(ValueError, match="without alleles"): + predictor.predict(["SIINFEKLL"]) + with pytest.raises(ValueError, match="without alleles"): + predictor.predict_peptides(["SIINFEKLL"]) + assert predictor.calls == [] # guarded before running any command