44"""
55
66from pathlib import Path
7- from typing import Any
7+ from typing import Any , Optional
88
99import click
1010import yaml
11- from jinja2 import Environment , FileSystemLoader
11+ from jinja2 import Environment , FileSystemLoader , Template
1212
1313
1414class ModuleParser :
1515 """Parse nf-core module meta.yml files and generate documentation."""
1616
17- def __init__ (self , modules_repo_path : str , output_dir : str ):
17+ modules_repo_path : Path
18+ output_dir : Path
19+ jinja_env : Environment
20+
21+ def __init__ (self , modules_repo_path : str , output_dir : str ) -> None :
1822 """Initialize the parser.
1923
2024 :param modules_repo_path: Path to the nf-core modules repository
@@ -68,32 +72,60 @@ def get_module_path_info(self, meta_file: Path, base_type: str) -> dict[str, str
6872 "subcategory" : "/" .join (parts [1 :- 1 ]) if len (parts ) > 2 else "" ,
6973 }
7074
71- def _detect_component_types (self , components : list [str ]) -> list [dict [str , str ]]:
75+ def _detect_component_types (
76+ self , components : list [str | dict [str , Any ]]
77+ ) -> list [dict [str , str ]]:
7278 """Detect whether components are modules or subworkflows.
7379
74- :param components: List of component names
75- :type components: list[str]
80+ :param components: List of component names or dicts with component info
81+ :type components: list[str | dict[str, Any] ]
7682 :returns: List of component info with type detection
7783 :rtype: list[dict[str, str]]
7884 """
79- component_info = []
85+ component_info : list [ dict [ str , str ]] = []
8086
8187 for component in components :
88+ # Handle both string format and dict format (for third-party modules)
89+ component_name : Optional [str ]
90+ git_remote : Optional [str ]
91+
92+ if isinstance (component , dict ):
93+ # Component is a dict like {"module_name": null, "git_remote": "..."}
94+ # Extract the component name (first key that's not git_remote)
95+ git_remote = component .get ("git_remote" )
96+ component_name = None
97+ for key in component .keys ():
98+ if key != "git_remote" :
99+ component_name = key
100+ break
101+
102+ if not component_name :
103+ continue
104+ else :
105+ # Component is a simple string
106+ component_name = component
107+ git_remote = None
108+
82109 # Check if component exists as a subworkflow
83- subworkflow_path = (
110+ subworkflow_path : Path = (
84111 self .modules_repo_path
85112 / "subworkflows"
86113 / "ebi-metagenomics"
87- / component
114+ / component_name
88115 / "meta.yml"
89116 )
90117
118+ component_type : str
91119 if subworkflow_path .exists ():
92120 component_type = "subworkflow"
93121 else :
94122 component_type = "module"
95123
96- component_info .append ({"name" : component , "type" : component_type })
124+ comp_info : dict [str , str ] = {"name" : component_name , "type" : component_type }
125+ if git_remote :
126+ comp_info ["git_remote" ] = git_remote
127+
128+ component_info .append (comp_info )
97129
98130 return component_info
99131
@@ -105,19 +137,19 @@ def _process_meta_data(self, meta_data: dict[str, Any]) -> dict[str, Any]:
105137 :returns: Processed meta data with flattened input/output structures
106138 :rtype: dict[str, Any]
107139 """
108- processed = meta_data .copy ()
140+ processed : dict [ str , Any ] = meta_data .copy ()
109141
110142 # Process tools section if it's a list
111143 if "tools" in processed and isinstance (processed ["tools" ], list ):
112- processed_tools = {}
144+ processed_tools : dict [ str , Any ] = {}
113145 for item in processed ["tools" ]:
114146 if isinstance (item , dict ):
115147 processed_tools .update (item )
116148 processed ["tools" ] = processed_tools
117149
118150 # Process input section if it's a list
119151 if "input" in processed and isinstance (processed ["input" ], list ):
120- processed_input = {}
152+ processed_input : dict [ str , Any ] = {}
121153 for item in processed ["input" ]:
122154 if isinstance (item , list ):
123155 # Handle nested list structure (like diamond/blastp)
@@ -133,7 +165,7 @@ def _process_meta_data(self, meta_data: dict[str, Any]) -> dict[str, Any]:
133165 if "output" in processed :
134166 if isinstance (processed ["output" ], list ):
135167 # Handle list format (like antismash)
136- processed_output = {}
168+ processed_output : dict [ str , Any ] = {}
137169 for item in processed ["output" ]:
138170 if isinstance (item , dict ):
139171 processed_output .update (item )
@@ -167,34 +199,36 @@ def generate_module_docs(self) -> None:
167199 :returns: None
168200 :rtype: None
169201 """
170- meta_files = self .find_meta_files ("modules" )
171- template = self .jinja_env .get_template ("module.md.j2" )
202+ meta_files : list [ Path ] = self .find_meta_files ("modules" )
203+ template : Template = self .jinja_env .get_template ("module.md.j2" )
172204
173- modules_output_dir = self .output_dir / "docs" / "modules"
205+ modules_output_dir : Path = self .output_dir / "docs" / "modules"
174206 modules_output_dir .mkdir (parents = True , exist_ok = True )
175207
176- module_index = []
208+ module_index : list [ dict [ str , str ]] = []
177209
178210 for meta_file in meta_files :
179211 try :
180- meta_data = self .parse_meta_yml (meta_file )
181- path_info = self .get_module_path_info (meta_file , "modules" )
212+ meta_data : dict [str , Any ] = self .parse_meta_yml (meta_file )
213+ path_info : dict [str , str ] = self .get_module_path_info (
214+ meta_file , "modules"
215+ )
182216
183217 # Process input/output for better template handling
184218 meta_data = self ._process_meta_data (meta_data )
185219
186220 # Create subdirectories as needed
187- module_dir = modules_output_dir / path_info ["category" ]
221+ module_dir : Path = modules_output_dir / path_info ["category" ]
188222 if path_info ["subcategory" ]:
189223 module_dir = module_dir / path_info ["subcategory" ]
190224 module_dir .mkdir (parents = True , exist_ok = True )
191225
192226 # Generate documentation
193- content = template .render (
227+ content : str = template .render (
194228 meta = meta_data , path_info = path_info , module_type = "module"
195229 )
196230
197- output_file = module_dir / f"{ path_info ['name' ]} .md"
231+ output_file : Path = module_dir / f"{ path_info ['name' ]} .md"
198232 with open (output_file , "w" ) as f :
199233 f .write (content )
200234
@@ -223,34 +257,36 @@ def generate_subworkflow_docs(self) -> None:
223257 :returns: None
224258 :rtype: None
225259 """
226- meta_files = self .find_meta_files ("subworkflows" )
227- template = self .jinja_env .get_template ("subworkflow.md.j2" )
260+ meta_files : list [ Path ] = self .find_meta_files ("subworkflows" )
261+ template : Template = self .jinja_env .get_template ("subworkflow.md.j2" )
228262
229- subworkflows_output_dir = self .output_dir / "docs" / "subworkflows"
263+ subworkflows_output_dir : Path = self .output_dir / "docs" / "subworkflows"
230264 subworkflows_output_dir .mkdir (parents = True , exist_ok = True )
231265
232- subworkflow_index = []
266+ subworkflow_index : list [ dict [ str , str ]] = []
233267
234268 for meta_file in meta_files :
235269 try :
236- meta_data = self .parse_meta_yml (meta_file )
237- path_info = self .get_module_path_info (meta_file , "subworkflows" )
270+ meta_data : dict [str , Any ] = self .parse_meta_yml (meta_file )
271+ path_info : dict [str , str ] = self .get_module_path_info (
272+ meta_file , "subworkflows"
273+ )
238274
239275 # Process input/output for better template handling
240276 meta_data = self ._process_meta_data (meta_data )
241277
242278 # Create subdirectories as needed
243- subworkflow_dir = subworkflows_output_dir / path_info ["category" ]
279+ subworkflow_dir : Path = subworkflows_output_dir / path_info ["category" ]
244280 if path_info ["subcategory" ]:
245281 subworkflow_dir = subworkflow_dir / path_info ["subcategory" ]
246282 subworkflow_dir .mkdir (parents = True , exist_ok = True )
247283
248284 # Generate documentation
249- content = template .render (
285+ content : str = template .render (
250286 meta = meta_data , path_info = path_info , module_type = "subworkflow"
251287 )
252288
253- output_file = subworkflow_dir / f"{ path_info ['name' ]} .md"
289+ output_file : Path = subworkflow_dir / f"{ path_info ['name' ]} .md"
254290 with open (output_file , "w" ) as f :
255291 f .write (content )
256292
@@ -274,34 +310,34 @@ def generate_subworkflow_docs(self) -> None:
274310 self ._generate_index (subworkflow_index , subworkflows_output_dir , "subworkflows" )
275311
276312 def _generate_index (
277- self , items : list [dict ], output_dir : Path , item_type : str
313+ self , items : list [dict [ str , str ] ], output_dir : Path , item_type : str
278314 ) -> None :
279315 """Generate index page for modules or subworkflows.
280316
281317 :param items: List of items to include in index
282- :type items: list[dict]
318+ :type items: list[dict[str, str] ]
283319 :param output_dir: Output directory
284320 :type output_dir: Path
285321 :param item_type: 'modules' or 'subworkflows'
286322 :type item_type: str
287323 :returns: None
288324 :rtype: None
289325 """
290- template = self .jinja_env .get_template ("index.md.j2" )
326+ template : Template = self .jinja_env .get_template ("index.md.j2" )
291327
292328 # Group by category
293- categories = {}
329+ categories : dict [ str , list [ dict [ str , str ]]] = {}
294330 for item in items :
295- category = item ["category" ]
331+ category : str = item ["category" ]
296332 if category not in categories :
297333 categories [category ] = []
298334 categories [category ].append (item )
299335
300- content = template .render (
336+ content : str = template .render (
301337 categories = categories , item_type = item_type , title = item_type .capitalize ()
302338 )
303339
304- index_file = output_dir / "index.md"
340+ index_file : Path = output_dir / "index.md"
305341 with open (index_file , "w" ) as f :
306342 f .write (content )
307343
@@ -315,7 +351,7 @@ def _generate_index(
315351 default = "." ,
316352 help = "Output directory for generated documentation (default: current directory)" ,
317353)
318- def main (modules_repo : str , output_dir : str ):
354+ def main (modules_repo : str , output_dir : str ) -> None :
319355 """Generate MkDocs documentation from nf-core modules and subworkflows.
320356
321357 :param modules_repo: Path to the nf-core modules repository
@@ -325,7 +361,7 @@ def main(modules_repo: str, output_dir: str):
325361 :returns: None
326362 :rtype: None
327363 """
328- parser = ModuleParser (modules_repo , output_dir )
364+ parser : ModuleParser = ModuleParser (modules_repo , output_dir )
329365
330366 print ("Generating module documentation..." )
331367 parser .generate_module_docs ()
0 commit comments