@@ -47,7 +47,24 @@ def _normalize_version(version: str | Version) -> Version:
4747 raise TypeError (f"Version must be str or Version, got { type (version ).__name__ } " )
4848
4949
50- def register_parser (software : str , min_version : str | None = None , max_version : str | None = None ):
50+ def _registry_key (software : str , subcommand : str | None ) -> str :
51+ """Build the registry lookup key from software and optional subcommand.
52+
53+ When a subcommand is given the key becomes 'software.subcommand'
54+ (e.g. 'samtools.coverage', 'samtools.stats'), forming a single composite
55+ key — not two separate registrations — so multiple parsers can share the
56+ same software name without colliding.
57+ """
58+ return f"{ software } .{ subcommand } " if subcommand else software
59+
60+
61+ def register_parser (
62+ software : str ,
63+ min_version : str | None = None ,
64+ max_version : str | None = None ,
65+ * ,
66+ subcommand : str | None = None ,
67+ ):
5168 """Decorator to register a parser for a range of versions.
5269
5370 Null values means either undefined or no upper range.
@@ -61,15 +78,17 @@ def wrapper(cls: ParserRegistryEntry):
6178 new_min = _normalize_version (min_version )
6279 new_max = _normalize_version (max_version )
6380
81+ key = _registry_key (software , subcommand )
82+
6483 # Fetch existing ranges for this software
65- existing_ranges = _PARSER_REGISTRY .get (software , [])
84+ existing_ranges = _PARSER_REGISTRY .get (key , [])
6685
6786 # Check for overlapping version ranges
6887 for span in existing_ranges :
6988 if not (new_max < span .min_version or new_min > span .max_version ):
7089 # Ranges overlap → safety error
7190 raise ValueError (
72- f"Cannot register parser { cls .__name__ } for software '{ software } ' "
91+ f"Cannot register parser { cls .__name__ } for software '{ key } ' "
7392 f"with version range [{ new_min } , { new_max } ] because it overlaps "
7493 f"with existing parser { span .entry .__name__ } range "
7594 f"[{ span .min_version } , { span .max_version } ]."
@@ -81,30 +100,36 @@ def wrapper(cls: ParserRegistryEntry):
81100 max_version = new_max ,
82101 entry = cls ,
83102 )
84- _PARSER_REGISTRY .setdefault (software , []).append (v_range )
103+ _PARSER_REGISTRY .setdefault (key , []).append (v_range )
85104
86105 return cls
87106
88107 return wrapper
89108
90109
91- def get_parser (software : str , * , version : str ) -> ParserRegistryEntry :
110+ def get_parser (
111+ software : str , * , version : str , subcommand : str | None = None
112+ ) -> ParserRegistryEntry :
92113 """Get parser from registry."""
93114 if not isinstance (software , str ):
94115 raise TypeError (f"`software` must be str, got { type (software ).__name__ } " )
95116
96- if software not in registered_softwares ():
97- raise UnsupportedSoftwareError (f"No parser registered for software: { software } " )
117+ key = _registry_key (software , subcommand )
118+
119+ if key not in registered_softwares ():
120+ raise UnsupportedSoftwareError (f"No parser registered for software: { key } " )
98121
99122 # Normalize version to PkgVersion
100123 v = _normalize_version (version )
101124
102- for span in sorted (_PARSER_REGISTRY [software ], key = lambda r : (r .min_version , r .max_version )):
125+ for span in sorted (_PARSER_REGISTRY [key ], key = lambda r : (r .min_version , r .max_version )):
103126 if span .min_version <= v <= span .max_version :
104127 return span .entry
105128
106129 # Return the correct error.
107- raise UnsupportedVersionError (f"No parser available for software '{ software } ' version { v } " )
130+ raise UnsupportedVersionError (
131+ f"No parser available for software '{ key } ' version { v } "
132+ )
108133
109134
110135def registered_softwares () -> list [str ]:
@@ -137,6 +162,7 @@ def run_parser(
137162 * ,
138163 version : str ,
139164 data : StreamOrPath ,
165+ subcommand : str | None = None ,
140166 want : set [AnalysisType ] | None = None ,
141167 parser_init : dict [str , Any ] | None = None ,
142168 ** parse_kwargs : Any ,
@@ -146,7 +172,7 @@ def run_parser(
146172 if not isinstance (software , (AnalysisSoftware , str )):
147173 raise ValueError (f"Invalid input for 'run_parser', got { type (software )} " )
148174
149- entry = get_parser (software , version = version )
175+ entry = get_parser (software , version = version , subcommand = subcommand )
150176 parse_fn = resolve_parser (entry , ** (parser_init or {}))
151177 ev = parse_fn (data , want = want , ** parse_kwargs )
152178 # add version to results
@@ -233,9 +259,7 @@ def _hydrate_raw_value(model_cls: ModelClass | TypeAdapter, raw_value: Any) -> A
233259 raise TypeError (f"Unsupported nested element model type: { type (model_cls ).__name__ } " )
234260
235261
236- def _hydrate_nested_fields (
237- result_obj : BaseModel , field_models : dict [str , ModelClass | TypeAdapter ]
238- ) -> BaseModel :
262+ def _hydrate_nested_fields (result_obj : BaseModel , field_models : dict [str , ModelClass | TypeAdapter ]) -> BaseModel :
239263 updates : dict [str , Any ] = {}
240264
241265 for field_name , model_cls in field_models .items ():
0 commit comments