From 0aafca7f73eaedaff2088a5ecdda68d2930b9431 Mon Sep 17 00:00:00 2001 From: Thomas Holder Date: Wed, 13 Dec 2023 15:18:39 +0100 Subject: [PATCH] Typing: Options --- propka/conformation_container.py | 26 ++++++++---- propka/coupled_groups.py | 9 ++-- propka/determinant.py | 9 +++- propka/input.py | 40 ++++++++++-------- propka/iterative.py | 2 +- propka/lib.py | 72 +++++++++++++++++++++----------- propka/molecular_container.py | 16 +++---- propka/output.py | 68 +++++++++++++++++++----------- propka/run.py | 7 +++- tests/test_lib.py | 27 ++++++++++++ 10 files changed, 184 insertions(+), 92 deletions(-) create mode 100644 tests/test_lib.py diff --git a/propka/conformation_container.py b/propka/conformation_container.py index 8616922..27601c4 100644 --- a/propka/conformation_container.py +++ b/propka/conformation_container.py @@ -6,7 +6,8 @@ Container data structure for molecular conformations. """ import logging import functools -from typing import Iterable, List, NoReturn, Optional, TYPE_CHECKING, Set +from typing import Callable, Dict, Iterable, Iterator, List, NoReturn, Optional, TYPE_CHECKING, Set + from propka.lib import Options from propka.version import Version @@ -26,6 +27,8 @@ from propka.parameters import Parameters _LOGGER = logging.getLogger(__name__) +CallableGroupToGroups = Callable[[Group], List[Group]] + #: A large number that gets multipled with the integer obtained from applying #: :func:`ord` to the atom chain ID. Used in calculating atom keys for @@ -276,7 +279,7 @@ class ConformationContainer: return penalised_labels @staticmethod - def share_determinants(groups): + def share_determinants(groups: Iterable[Group]): """Share sidechain, backbone, and Coloumb determinants between groups. Args: @@ -286,7 +289,7 @@ class ConformationContainer: types = ['sidechain', 'backbone', 'coulomb'] for type_ in types: # find maximum value for each determinant - max_dets = {} + max_dets: Dict[Group, float] = {} for group in groups: for det in group.determinants[type_]: # update max dets @@ -302,7 +305,11 @@ class ConformationContainer: for group in groups: group.set_determinant(new_determinant, type_) - def get_coupled_systems(self, groups, get_coupled_groups): + def get_coupled_systems( + self, + groups: Iterable[Group], + get_coupled_groups: CallableGroupToGroups, + ) -> Iterator[Set[Group]]: """A generator that yields covalently coupled systems. Args: @@ -314,15 +321,16 @@ class ConformationContainer: groups = set(groups) while len(groups) > 0: # extract a system of coupled groups ... - system = set() + system: Set[Group] = set() self.get_a_coupled_system_of_groups( groups.pop(), system, get_coupled_groups) # ... and remove them from the list groups -= system yield system - def get_a_coupled_system_of_groups(self, new_group, coupled_groups, - get_coupled_groups): + def get_a_coupled_system_of_groups(self, new_group: Group, + coupled_groups: Set[Group], + get_coupled_groups: CallableGroupToGroups): """Set up coupled systems of groups. Args: @@ -353,7 +361,7 @@ class ConformationContainer: reference=reference) return ddg - def calculate_charge(self, parameters, ph: float): + def calculate_charge(self, parameters: Parameters, ph: float): """Calculate charge for folded and unfolded states. Args: @@ -371,7 +379,7 @@ class ConformationContainer: state='folded') return unfolded, folded - def get_backbone_groups(self): + def get_backbone_groups(self) -> List[Group]: """Get backbone groups needed for the pKa calculations. Returns: diff --git a/propka/coupled_groups.py b/propka/coupled_groups.py index 48aa8ba..7ed0e87 100644 --- a/propka/coupled_groups.py +++ b/propka/coupled_groups.py @@ -6,9 +6,11 @@ Describe and analyze energetic coupling between groups. """ import logging import itertools +from typing import Optional import propka.lib from propka.group import Group from propka.output import make_interaction_map +from propka.parameters import Parameters _LOGGER = logging.getLogger(__name__) @@ -16,9 +18,8 @@ _LOGGER = logging.getLogger(__name__) class NonCovalentlyCoupledGroups: """Groups that are coupled without covalent bonding.""" - def __init__(self): - self.parameters = None - self.do_prot_stat = True + parameters: Optional[Parameters] = None + do_prot_stat = True def is_coupled_protonation_state_probability(self, group1, group2, energy_method, @@ -264,7 +265,7 @@ class NonCovalentlyCoupledGroups: _LOGGER.info(swap_info) @staticmethod - def get_interaction(group1, group2, include_side_chain_hbs=True): + def get_interaction(group1: Group, group2: Group, include_side_chain_hbs=True): """Get interaction energy between two groups. Args: diff --git a/propka/determinant.py b/propka/determinant.py index 03f06d7..741390f 100644 --- a/propka/determinant.py +++ b/propka/determinant.py @@ -15,6 +15,11 @@ Provides the :class:`Determinant` class. """ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from propka.group import Group + class Determinant: """Determinant class. @@ -25,7 +30,7 @@ class Determinant: TODO - figure out what this class does. """ - def __init__(self, group, value): + def __init__(self, group: "Group", value: float): """Initialize the object. Args: @@ -36,7 +41,7 @@ class Determinant: self.label = group.label self.value = value - def add(self, value): + def add(self, value: float): """Increment determinant value. Args: diff --git a/propka/input.py b/propka/input.py index e5658d8..6925d4c 100644 --- a/propka/input.py +++ b/propka/input.py @@ -9,8 +9,7 @@ Input routines. Methods to read PROPKA input files (:func:`read_propka` and :func:`get_atom_lines_from_input`) have been removed. """ -import typing -from typing import Iterator, Tuple, Union +from typing import IO, ContextManager, Dict, Iterable, Iterator, Optional, Tuple import contextlib import io import zipfile @@ -19,19 +18,18 @@ from propka.lib import protein_precheck from propka.atom import Atom from propka.conformation_container import ConformationContainer from propka.molecular_container import MolecularContainer +from propka.output import _PathArg, _PathLikeTypes, _TextIOSource from propka.parameters import Parameters -def open_file_for_reading( - input_file: typing.Union[str, Path, typing.TextIO] -) -> typing.ContextManager[typing.TextIO]: +def open_file_for_reading(input_file: _TextIOSource) -> ContextManager[IO[str]]: """Open file or file-like stream for reading. Args: input_file: path to file or file-like object. If file-like object, then will attempt seek(0). """ - if not isinstance(input_file, (str, Path)): + if not isinstance(input_file, _PathLikeTypes): input_file.seek(0) return contextlib.nullcontext(input_file) @@ -48,7 +46,11 @@ def open_file_for_reading( return contextlib.closing(open(input_file, 'rt')) -def read_molecule_file(filename: str, mol_container: MolecularContainer, stream=None) -> MolecularContainer: +def read_molecule_file( + filename: _PathArg, + mol_container: MolecularContainer, + stream: Optional[IO[str]] = None, +) -> MolecularContainer: """Read input file or stream (PDB or PROPKA) for a molecular container Args: @@ -96,11 +98,7 @@ def read_molecule_file(filename: str, mol_container: MolecularContainer, stream= input_path = Path(filename) mol_container.name = input_path.stem input_file_extension = input_path.suffix - - if stream is not None: - input_file = stream - else: - input_file = filename + input_file = filename if stream is None else stream if input_file_extension.lower() == '.pdb': # input is a pdb file. read in atoms and top up containers to make @@ -133,7 +131,7 @@ def read_molecule_file(filename: str, mol_container: MolecularContainer, stream= return mol_container -def read_parameter_file(input_file: Union[Path, str], parameters: Parameters) -> Parameters: +def read_parameter_file(input_file: _PathArg, parameters: Parameters) -> Parameters: """Read a parameter file. Args: @@ -161,8 +159,13 @@ def conformation_sorter(conf: str) -> int: return model*100+ord(altloc) -def get_atom_lines_from_pdb(pdb_file, ignore_residues=[], keep_protons=False, - tags=['ATOM ', 'HETATM'], chains=None) -> Iterator[Tuple[str, Atom]]: +def get_atom_lines_from_pdb( + pdb_file: _TextIOSource, + ignore_residues: Iterable[str] = (), + keep_protons: bool = False, + tags: Iterable[str] = ('ATOM ', 'HETATM'), + chains: Optional[Iterable[str]] = None, +) -> Iterator[Tuple[str, Atom]]: """Get atom lines from PDB file. Args: @@ -228,7 +231,8 @@ def get_atom_lines_from_pdb(pdb_file, ignore_residues=[], keep_protons=False, terminal = None -def read_pdb(pdb_file, parameters, molecule): +def read_pdb(pdb_file: _TextIOSource, parameters: Parameters, + molecule: MolecularContainer): """Parse a PDB file. Args: @@ -240,7 +244,7 @@ def read_pdb(pdb_file, parameters, molecule): 1. list of conformations 2. list of names """ - conformations = {} + conformations: Dict[str, ConformationContainer] = {} # read in all atoms in the file lines = get_atom_lines_from_pdb( pdb_file, ignore_residues=parameters.ignore_residues, @@ -253,4 +257,4 @@ def read_pdb(pdb_file, parameters, molecule): conformations[name].add_atom(atom) # make a sorted list of conformation names names = sorted(conformations.keys(), key=conformation_sorter) - return [conformations, names] + return conformations, names diff --git a/propka/iterative.py b/propka/iterative.py index b4e9167..2ada994 100644 --- a/propka/iterative.py +++ b/propka/iterative.py @@ -304,7 +304,7 @@ def add_determinants(iterative_interactions: List[Interaction], version: Version for itres in iteratives: for type_ in ['sidechain', 'backbone', 'coulomb']: for interaction in itres.determinants[type_]: - value = interaction[1] + value: float = interaction[1] if value > UNK_MIN_VALUE or value < -UNK_MIN_VALUE: group = interaction[0] new_det = Determinant(group, value) diff --git a/propka/lib.py b/propka/lib.py index 816baa9..17dfc86 100644 --- a/propka/lib.py +++ b/propka/lib.py @@ -5,16 +5,20 @@ Set-up of a PROPKA calculation Implements many of the main functions used to call PROPKA. """ -import sys import logging import argparse from pathlib import Path -from typing import List, TYPE_CHECKING +from typing import Iterable, Iterator, List, TYPE_CHECKING, NoReturn, Optional, Tuple, TypeVar if TYPE_CHECKING: from propka.atom import Atom +T = TypeVar("T") +Number = TypeVar("Number", int, float) + +_T_RESIDUE_TUPLE = Tuple[str, int, str] + _LOGGER = logging.getLogger(__name__) @@ -23,7 +27,28 @@ EXPECTED_ATOM_NUMBERS = {'ALA': 5, 'ARG': 11, 'ASN': 8, 'ASP': 8, 'CYS': 6, 'LEU': 8, 'LYS': 9, 'MET': 8, 'PHE': 11, 'PRO': 7, 'SER': 6, 'THR': 7, 'TRP': 14, 'TYR': 12, 'VAL': 7} -Options = argparse.Namespace + +class Options: + # Note: All the "NoReturn" members appear to be unused + alignment: NoReturn # Optional[List[str]] + chains: Optional[List[str]] + display_coupled_residues: bool = False + filenames: List[str] # List[Path]? + grid: Tuple[float, float, float] = (0.0, 14.0, 0.1) + input_pdb: str # Path? + keep_protons: bool = False + log_level: str = 'INFO' + mutations: NoReturn # Optional[List[str]] + mutator: NoReturn # Optional[str] # alignment/scwrl/jackal + mutator_options: NoReturn # Optional[List[str]] + pH: NoReturn # float = 7.0 + parameters: Path + protonate_all: bool = False + reference: NoReturn # str = 'neutral' + reuse_ligand_mol2_file: bool = False # only used by unused function + thermophiles: NoReturn # Optional[List[str]] + titrate_only: Optional[List[_T_RESIDUE_TUPLE]] + window: Tuple[float, float, float] = (0.0, 14.0, 1.0) def protein_precheck(conformations, names): @@ -112,11 +137,11 @@ def make_molecule(atom: "Atom", atoms: List["Atom"]): return res_atoms -def make_grid(min_, max_, step): +def make_grid(min_: Number, max_: Number, step: Number) -> Iterator[Number]: """Make a grid across the specified tange. - TODO - figure out if this duplicates existing generators like `range` or - numpy function. + Like range() for integers or numpy.arange() for floats, except that `max_` + is not excluded from the range. Args: min_: minimum value of grid @@ -129,7 +154,7 @@ def make_grid(min_, max_, step): x += step -def generate_combinations(interactions): +def generate_combinations(interactions: Iterable[T]) -> List[List[T]]: """Generate combinations of interactions. Args: @@ -137,14 +162,14 @@ def generate_combinations(interactions): Returns: list of combinations """ - res = [[]] + res: List[List[T]] = [[]] for interaction in interactions: res = make_combination(res, interaction) res.remove([]) return res -def make_combination(combis, interaction): +def make_combination(combis: List[List[T]], interaction: T) -> List[List[T]]: """Make a specific set of combinations. Args: @@ -160,7 +185,7 @@ def make_combination(combis, interaction): return res -def parse_res_string(res_str): +def parse_res_string(res_str: str) -> _T_RESIDUE_TUPLE: """Parse a residue string. Args: @@ -189,6 +214,16 @@ def parse_res_string(res_str): return chain, resnum, inscode +def parse_res_list(titrate_only: str): + res_list: List[_T_RESIDUE_TUPLE] = [] + for res_str in titrate_only.split(','): + try: + res_list.append(parse_res_string(res_str)) + except ValueError as ex: + raise argparse.ArgumentTypeError(f'{ex}: "{res_str:s}"') + return res_list + + def build_parser(parser=None): """Build an argument parser for PROPKA. @@ -233,6 +268,7 @@ def build_parser(parser=None): '" " for chains without ID [all]')) group.add_argument( "-i", "--titrate_only", dest="titrate_only", + type=parse_res_list, help=('Treat only the specified residues as titratable. Value should ' 'be a comma-separated list of "chain:resnum" values; for ' 'example: -i "A:10,A:11"')) @@ -252,8 +288,8 @@ def build_parser(parser=None): "--version", action="version", version=f"%(prog)s {propka.__version__}") group.add_argument( "-p", "--parameters", dest="parameters", - default=str(Path(__file__).parent / "propka.cfg"), - help="set the parameter file [{default:s}]") + type=Path, default=Path(__file__).parent / "propka.cfg", + help="set the parameter file") try: group.add_argument( "--log-level", @@ -325,18 +361,6 @@ def loadOptions(args=None) -> Options: # adding specified filenames to arguments options.filenames.append(options.input_pdb) - # Convert titrate_only string to a list of (chain, resnum) items: - if options.titrate_only is not None: - res_list = [] - for res_str in options.titrate_only.split(','): - try: - chain, resnum, inscode = parse_res_string(res_str) - except ValueError: - _LOGGER.critical( - 'Invalid residue string: "{0:s}"'.format(res_str)) - sys.exit(1) - res_list.append((chain, resnum, inscode)) - options.titrate_only = res_list # Set the no-print variable level = getattr(logging, options.log_level) _LOGGER.setLevel(level) diff --git a/propka/molecular_container.py b/propka/molecular_container.py index 1cc587b..394ae6f 100644 --- a/propka/molecular_container.py +++ b/propka/molecular_container.py @@ -157,7 +157,7 @@ class MolecularContainer: conformation='AVR', reference=reference) def get_folding_profile(self, conformation='AVR', reference="neutral", - grid=[0., 14., 0.1]): + grid: Tuple[float, float, float] = (0., 14., 0.1)): """Get a folding profile. Args: @@ -174,25 +174,25 @@ class MolecularContainer: 4. stability_range """ # calculate stability profile - profile = [] + profile: List[Tuple[float, float]] = [] for ph in make_grid(*grid): conf = self.conformations[conformation] ddg = conf.calculate_folding_energy(ph=ph, reference=reference) - profile.append([ph, ddg]) + profile.append((ph, ddg)) # find optimum - opt = [None, 1e6] + opt: Tuple[Optional[float], float] = (None, 1e6) for point in profile: opt = min(opt, point, key=lambda v: v[1]) # find values within 80 % of optimum - range_80pct = [None, None] + range_80pct: Tuple[Optional[float], Optional[float]] = (None, None) values_within_80pct = [p[0] for p in profile if p[1] < 0.8*opt[1]] if len(values_within_80pct) > 0: - range_80pct = [min(values_within_80pct), max(values_within_80pct)] + range_80pct = (min(values_within_80pct), max(values_within_80pct)) # find stability range - stability_range = [None, None] + stability_range: Tuple[Optional[float], Optional[float]] = (None, None) stable_values = [p[0] for p in profile if p[1] < 0.0] if len(stable_values) > 0: - stability_range = [min(stable_values), max(stable_values)] + stability_range = (min(stable_values), max(stable_values)) return profile, opt, range_80pct, stability_range def get_charge_profile(self, conformation: str = 'AVR', grid=[0., 14., .1]): diff --git a/propka/output.py b/propka/output.py index 20ec4a7..d9d8617 100644 --- a/propka/output.py +++ b/propka/output.py @@ -12,13 +12,29 @@ Output routines. import logging from datetime import date from decimal import Decimal +from os import PathLike +from pathlib import Path +from typing import IO, AnyStr, List, NoReturn, Optional, Union, TYPE_CHECKING +import warnings + +from .parameters import Parameters from . import __version__ +if TYPE_CHECKING: + from .atom import Atom + from .conformation_container import ConformationContainer + from .molecular_container import MolecularContainer + +# https://docs.python.org/3/glossary.html#term-path-like-object +_PathLikeTypes = (PathLike, str) +_PathArg = Union[PathLike, str] +_IOSource = Union[IO[AnyStr], PathLike, str] +_TextIOSource = _IOSource[str] _LOGGER = logging.getLogger(__name__) -def open_file_for_writing(input_file): +def open_file_for_writing(input_file: _TextIOSource) -> IO[str]: """Open file or file-like stream for writing. TODO - convert this to a context manager. @@ -27,17 +43,13 @@ def open_file_for_writing(input_file): input_file: path to file or file-like object. If file-like object, then will attempt to get file mode. """ - try: - if not input_file.writable(): - raise IOError("File/stream not open for writing") - return input_file - except AttributeError: - pass - try: - file_ = open(input_file, 'wt') - except FileNotFoundError: - raise Exception('Could not open {0:s}'.format(input_file)) - return file_ + if isinstance(input_file, _PathLikeTypes): + return open(input_file, 'wt') + + if not input_file.writable(): + raise IOError("File/stream not open for writing") + + return input_file def write_file(filename, lines): @@ -47,6 +59,7 @@ def write_file(filename, lines): filename: name of file lines: lines to write to file """ + warnings.warn("unused and untested by propka") file_ = open_file_for_writing(filename) for line in lines: file_.write("{0:s}\n".format(line)) @@ -101,17 +114,22 @@ def write_pdb_for_protein( pdbfile.close() -def write_pdb_for_conformation(conformation, filename): +def write_pdb_for_conformation(conformation: "ConformationContainer", + filename: _PathArg): """Write PDB conformation to a file. Args: conformation: conformation container filename: filename for output """ + warnings.warn("unused and untested by propka") write_pdb_for_atoms(conformation.atoms, filename) -def write_pka(protein, parameters, filename=None, conformation='1A', +def write_pka(protein: "MolecularContainer", + parameters: Parameters, + filename: Optional[_PathArg] = None, + conformation='1A', reference="neutral", _="folding", verbose=False, __=None): """Write the pKa-file based on the given protein. @@ -129,8 +147,6 @@ def write_pka(protein, parameters, filename=None, conformation='1A', verbose = True if filename is None: filename = "{0:s}.pka".format(protein.name) - # TODO - this would be much better with a context manager - file_ = open(filename, 'w') if verbose: _LOGGER.info("Writing {0:s}".format(filename)) # writing propka header @@ -149,11 +165,10 @@ def write_pka(protein, parameters, filename=None, conformation='1A', # printing Protein Charge Profile str_ += get_charge_profile_section(protein, conformation=conformation) # now, writing the pka text to file - file_.write(str_) - file_.close() + Path(filename).write_text(str_, encoding="utf-8") -def print_tm_profile(protein, reference="neutral", window=[0., 14., 1.], +def print_tm_profile(protein: NoReturn, reference="neutral", window=[0., 14., 1.], __=[0., 0.], tms=None, ref=None, _=False, options=None): """Print Tm profile. @@ -186,7 +201,7 @@ def print_tm_profile(protein, reference="neutral", window=[0., 14., 1.], _LOGGER.info(str_) -def print_result(protein, conformation, parameters): +def print_result(protein: "MolecularContainer", conformation: str, parameters: Parameters): """Prints all resulting output from determinants and down. Args: @@ -197,7 +212,7 @@ def print_result(protein, conformation, parameters): print_pka_section(protein, conformation, parameters) -def print_pka_section(protein, conformation, parameters): +def print_pka_section(protein: "MolecularContainer", conformation: str, parameters: Parameters): """Prints out pKa section of results. Args: @@ -212,7 +227,7 @@ def print_pka_section(protein, conformation, parameters): _LOGGER.info("pKa summary:\n%s", str_) -def get_determinant_section(protein, conformation, parameters): +def get_determinant_section(protein: "MolecularContainer", conformation: str, parameters: Parameters): """Returns string with determinant section of results. Args: @@ -244,7 +259,8 @@ def get_determinant_section(protein, conformation, parameters): return str_ -def get_summary_section(protein, conformation, parameters): +def get_summary_section(protein: "MolecularContainer", conformation: str, + parameters: Parameters): """Returns string with summary section of the results. Args: @@ -266,7 +282,8 @@ def get_summary_section(protein, conformation, parameters): def get_folding_profile_section( - protein, conformation='AVR', direction="folding", reference="neutral", + protein: "MolecularContainer", + conformation='AVR', direction="folding", reference="neutral", window=[0., 14., 1.0], _=False, __=None): """Returns string with the folding profile section of the results. @@ -373,6 +390,7 @@ def write_scwrl_sequence_file(sequence, filename="x-ray.seq", _=None): TODO - figure out what this is """ + warnings.warn("unused and untested by propka") with open(filename, 'w') as file_: start = 0 while len(sequence[start:]) > 60: @@ -536,7 +554,7 @@ def make_interaction_map(name, list_, interaction): return res -def write_pdb_for_atoms(atoms, filename, make_conect_section=False): +def write_pdb_for_atoms(atoms: List["Atom"], filename: _PathArg, make_conect_section=False): """Write out PDB file for atoms. Args: diff --git a/propka/run.py b/propka/run.py index ba8d53f..5e4bf65 100644 --- a/propka/run.py +++ b/propka/run.py @@ -12,10 +12,12 @@ function. If similar functionality is desired from a Python script """ import logging import sys +from typing import IO, Iterable, Optional from propka.lib import loadOptions from propka.input import read_parameter_file, read_molecule_file from propka.parameters import Parameters from propka.molecular_container import MolecularContainer +from propka.output import _PathArg _LOGGER = logging.getLogger(__name__) @@ -44,7 +46,9 @@ def main(optargs=None): my_molecule.write_pka() -def single(filename: str, optargs: tuple = (), stream=None, +def single(filename: _PathArg, + optargs: Iterable[str] = (), + stream: Optional[IO[str]] = None, write_pka: bool = True): """Run a single PROPKA calculation using ``filename`` as input. @@ -105,6 +109,7 @@ def single(filename: str, optargs: tuple = (), stream=None, .. versionchanged:: 3.4.0 Removed ability to write out PROPKA input files. """ + filename = str(filename) # Deal with input optarg options optargs = tuple(optargs) optargs += (filename,) diff --git a/tests/test_lib.py b/tests/test_lib.py new file mode 100644 index 0000000..8a9c66e --- /dev/null +++ b/tests/test_lib.py @@ -0,0 +1,27 @@ +import propka.lib as m + +import argparse +import pytest + + +def test_parse_res_string(): + assert m.parse_res_string("C:123") == ("C", 123, " ") + assert m.parse_res_string("C:123B") == ("C", 123, "B") + assert m.parse_res_string("ABC:123x") == ("ABC", 123, "x") + with pytest.raises(ValueError): + m.parse_res_string("C:B123") + with pytest.raises(ValueError): + m.parse_res_string("123B") + with pytest.raises(ValueError): + m.parse_res_string("C:123:B") + + +def test_parse_res_list(): + assert m.parse_res_list("C:123") == [("C", 123, " ")] + assert m.parse_res_list("ABC:123,D:4,F:56X") == [ + ("ABC", 123, " "), + ("D", 4, " "), + ("F", 56, "X"), + ] + with pytest.raises(argparse.ArgumentTypeError): + m.parse_res_list("C:B123")