1010import os
1111import re
1212import sys
13- from copy import deepcopy
13+ from concurrent . futures import ProcessPoolExecutor
1414
1515import yaml
1616
2323NVIDIA_STYLE_TAG_ID = "nvidia-selector-css"
2424FA_TAG_ID = "rapids-fa-tag"
2525
26+ LIB_PATH_DICT = None
27+ PROJECT_TO_VERSIONS_DICT = None
28+ SELECTOR_PROJECT_NAMES = None
29+
30+
31+ def initialize_worker (
32+ lib_path_dict : dict ,
33+ project_to_versions_dict : dict ,
34+ selector_project_names : set [str ],
35+ ) -> None :
36+ """Initializes read-only configuration used by each worker process."""
37+ global LIB_PATH_DICT , PROJECT_TO_VERSIONS_DICT , SELECTOR_PROJECT_NAMES
38+ LIB_PATH_DICT = lib_path_dict
39+ PROJECT_TO_VERSIONS_DICT = project_to_versions_dict
40+ SELECTOR_PROJECT_NAMES = selector_project_names
41+
42+
43+ def customize_manifest_file (filepath : str ) -> None :
44+ """Customizes one HTML file from the generated manifest."""
45+ project_name = get_lib_from_fp (
46+ lib_path_dict = LIB_PATH_DICT ,
47+ filepath = filepath ,
48+ )
49+ main (
50+ filepath = filepath ,
51+ lib_path_dict = LIB_PATH_DICT ,
52+ project_name = project_name ,
53+ versions_dict = PROJECT_TO_VERSIONS_DICT [project_name ],
54+ selector_project_names = SELECTOR_PROJECT_NAMES ,
55+ )
56+
57+
58+ def process_count () -> int :
59+ """Returns the number of CPUs available to this process, like nproc."""
60+ if hasattr (os , "sched_getaffinity" ):
61+ return max (1 , len (os .sched_getaffinity (0 )))
62+ return max (1 , os .cpu_count () or 1 )
63+
2664
2765def get_version_from_fp (* , filepath : str , versions_dict : dict ):
2866 """
@@ -130,7 +168,6 @@ def create_version_options(
130168 option_href = version_path
131169 version_text = f"{ version_name } ({ version_number_str } )"
132170 if version_name == doc_version ["name" ]:
133- print (f"default version: { version_name } " )
134171 is_selected = True
135172 options .append (
136173 {"selected" : is_selected , "href" : option_href , "text" : version_text }
@@ -159,7 +196,6 @@ def create_library_options(
159196 continue
160197 is_selected = False
161198 if lib == project_name :
162- print (f"default lib: { lib } " )
163199 is_selected = True
164200 options .append ({"selected" : is_selected , "href" : option_href , "text" : lib })
165201
@@ -225,21 +261,11 @@ def create_pixel_tags(soup):
225261 return [head_tag , body_tag ]
226262
227263
228- def uses_nvidia_sphinx_theme (soup ) -> bool :
229- """
230- Returns whether the document already uses the NVIDIA Sphinx Theme.
231- """
232- return any (
233- "nvidia-sphinx-theme" in link .get ("href" , "" )
234- for link in soup .find_all ("link" , href = True )
235- )
236-
237-
238- def create_css_link_tag (soup ):
264+ def create_css_link_tag (soup , * , is_nvidia_theme : bool ):
239265 """
240266 Creates and returns the stylesheet tag for the injected selectors.
241267 """
242- if uses_nvidia_sphinx_theme ( soup ) :
268+ if is_nvidia_theme :
243269 return soup .new_tag (
244270 "link" ,
245271 id = NVIDIA_STYLE_TAG_ID ,
@@ -253,51 +279,86 @@ def create_css_link_tag(soup):
253279 return script_tag
254280
255281
256- def delete_element (soup , selector ):
257- """
258- Deletes element from soup object if it already exists
259- """
260- try :
261- soup .select (f"{ selector } " )[0 ].extract ()
262- except Exception :
263- pass
264-
265-
266- def delete_rapids_custom_css_links (soup ):
282+ def delete_rapids_custom_css_links (links ):
267283 """
268284 Deletes global RAPIDS custom CSS links from NVIDIA-themed pages.
269285 """
270- for link in soup .find_all ("link" , href = True ):
271- if link ["href" ].endswith ("/assets/css/custom.css" ):
272- link .extract ()
286+ for link in links :
287+ link .extract ()
273288
274289
275- def delete_existing_elements (soup ):
290+ def delete_existing_elements (elements , * , doc_type : str , reference_el ):
276291 """
277292 Deletes any existing page elements to prevent duplicates on the page
278293 """
279- doxygen_title_area = "#titlearea > table"
280- sphinx_home_btn = ".wy-side-nav-search .icon.icon-home"
281- sphinx_doc_version = ".wy-side-nav-search .version"
282- existing_jtd_container = "#rapids-jtd-container"
283- existing_pydata_container = "#rapids-pydata-container"
284- existing_doxygen_container = "#rapids-doxygen-container"
285-
286- for element in [
287- existing_jtd_container ,
288- existing_pydata_container ,
289- existing_doxygen_container ,
290- sphinx_doc_version ,
291- sphinx_home_btn ,
292- doxygen_title_area ,
293- f"#{ SCRIPT_TAG_ID } " ,
294- f"#{ STYLE_TAG_ID } " ,
295- f"#{ NVIDIA_STYLE_TAG_ID } " ,
296- f"#{ FA_TAG_ID } " ,
297- f"#{ PIXEL_SRC_TAG_ID } " ,
298- f"#{ PIXEL_INVOCATION_TAG_ID } " ,
299- ]:
300- delete_element (soup , element )
294+ for element in elements :
295+ element .extract ()
296+
297+ if doc_type == "doxygen" :
298+ if table := reference_el .find ("table" , recursive = False ):
299+ table .extract ()
300+
301+ if doc_type == "jtd" :
302+ if version := reference_el .find (class_ = "version" ):
303+ version .extract ()
304+ if home_button := reference_el .find (
305+ lambda tag : {"icon" , "icon-home" }.issubset (tag .get ("class" , []))
306+ ):
307+ home_button .extract ()
308+
309+
310+ def inspect_document (soup , * , filepath : str ):
311+ """Collects theme and customization state in one document traversal."""
312+ removable_ids = {
313+ "rapids-jtd-container" ,
314+ "rapids-pydata-container" ,
315+ "rapids-doxygen-container" ,
316+ SCRIPT_TAG_ID ,
317+ STYLE_TAG_ID ,
318+ NVIDIA_STYLE_TAG_ID ,
319+ FA_TAG_ID ,
320+ PIXEL_SRC_TAG_ID ,
321+ PIXEL_INVOCATION_TAG_ID ,
322+ }
323+ existing_elements = []
324+ rapids_css_links = []
325+ references = {}
326+ is_nvidia_theme = False
327+
328+ for element in soup .find_all (True ):
329+ element_id = element .get ("id" )
330+ if element_id in removable_ids :
331+ existing_elements .append (element )
332+
333+ classes = element .get ("class" , [])
334+ if "wy-side-nav-search" in classes and "jtd" not in references :
335+ references ["jtd" ] = element
336+ elif element_id == "titlearea" and "doxygen" not in references :
337+ references ["doxygen" ] = element
338+ elif "bd-sidebar" in classes and "pydata" not in references :
339+ references ["pydata" ] = element
340+
341+ if element .name == "link" and (href := element .get ("href" )):
342+ if "nvidia-sphinx-theme" in href :
343+ is_nvidia_theme = True
344+ if element_id not in removable_ids and href .endswith (
345+ "/assets/css/custom.css"
346+ ):
347+ rapids_css_links .append (element )
348+
349+ for doc_type in ("jtd" , "doxygen" , "pydata" ):
350+ if doc_type in references :
351+ return (
352+ doc_type ,
353+ references [doc_type ],
354+ is_nvidia_theme ,
355+ existing_elements ,
356+ rapids_css_links ,
357+ )
358+
359+ raise UnsupportedThemeError (
360+ f"Couldn't identify { filepath } as a supported theme type. Skipping file."
361+ )
301362
302363
303364class UnsupportedThemeError (ValueError ):
@@ -309,33 +370,6 @@ class UnsupportedThemeError(ValueError):
309370 pass
310371
311372
312- def get_theme_info (soup , * , filepath : str ):
313- """
314- Determines what theme a given HTML file is using or exits if it's
315- not able to be determined. Returns a string identifier and reference element
316- that is used for inserting the library/version selectors to the doc.
317- """
318- # Sphinx Themes
319- jtd_identifier = ".wy-side-nav-search" # Just-the-docs theme
320- pydata_identifier = ".bd-sidebar" # Pydata theme
321-
322- # Doxygen
323- doxygen_identifier = "#titlearea"
324-
325- if soup .select (jtd_identifier ):
326- return "jtd" , soup .select (jtd_identifier )[0 ]
327-
328- if soup .select (doxygen_identifier ):
329- return "doxygen" , soup .select (doxygen_identifier )[0 ]
330-
331- if soup .select (pydata_identifier ):
332- return "pydata" , soup .select (pydata_identifier )[0 ]
333-
334- raise UnsupportedThemeError (
335- f"Couldn't identify { filepath } as a supported theme type. Skipping file."
336- )
337-
338-
339373def main (
340374 * ,
341375 filepath : str ,
@@ -349,21 +383,29 @@ def main(
349383 parse the file and add library/version selectors and a Home button
350384 """
351385
352- print (f"--- { filepath } ---" )
353-
354386 with open (filepath ) as fp :
355387 soup = BeautifulSoup (fp , "html5lib" )
356388
357389 try :
358- doc_type , reference_el = get_theme_info (soup , filepath = filepath )
390+ (
391+ doc_type ,
392+ reference_el ,
393+ is_nvidia_theme ,
394+ existing_elements ,
395+ rapids_css_links ,
396+ ) = inspect_document (soup , filepath = filepath )
359397 except UnsupportedThemeError as err :
360398 print (f"{ str (err )} " , file = sys .stderr )
361399 return
362400
363401 # Delete any existing added/unnecessary elements
364- delete_existing_elements (soup )
365- if uses_nvidia_sphinx_theme (soup ):
366- delete_rapids_custom_css_links (soup )
402+ delete_existing_elements (
403+ existing_elements ,
404+ doc_type = doc_type ,
405+ reference_el = reference_el ,
406+ )
407+ if is_nvidia_theme :
408+ delete_rapids_custom_css_links (rapids_css_links )
367409
368410 # Add Font Awesome to Doxygen for icons
369411 if doc_type == "doxygen" :
@@ -392,7 +434,7 @@ def main(
392434 container = soup .new_tag ("div" , id = f"rapids-{ doc_type } -container" )
393435 script_tag = create_script_tag (soup )
394436 [pix_head_tag , pix_body_tag ] = create_pixel_tags (soup )
395- style_tab = create_css_link_tag (soup )
437+ style_tab = create_css_link_tag (soup , is_nvidia_theme = is_nvidia_theme )
396438
397439 # Append elements to container
398440 container .append (home_btn_container )
@@ -428,21 +470,21 @@ def main(
428470 SELECTOR_PROJECT_NAMES = get_selector_project_names (docs_yml_path = DOCS_YML_PATH )
429471
430472 with open (MANIFEST_FILEPATH ) as manifest_file :
431- for line in manifest_file :
432- filepath = line . strip ()
433-
434- lib_path_dict = deepcopy ( LIB_PATH_DICT )
435-
436- # determine project name (e.g. 'cudf')
437- project_name = get_lib_from_fp (
438- lib_path_dict = lib_path_dict ,
439- filepath = filepath ,
440- )
441-
442- main (
443- filepath = filepath ,
444- lib_path_dict = lib_path_dict ,
445- project_name = project_name ,
446- versions_dict = deepcopy ( PROJECT_TO_VERSIONS_DICT [ project_name ]) ,
447- selector_project_names = SELECTOR_PROJECT_NAMES ,
448- )
473+ filepaths = [ line . strip () for line in manifest_file if line . strip ()]
474+
475+ with ProcessPoolExecutor (
476+ max_workers = process_count (),
477+ initializer = initialize_worker ,
478+ initargs = (
479+ LIB_PATH_DICT ,
480+ PROJECT_TO_VERSIONS_DICT ,
481+ SELECTOR_PROJECT_NAMES ,
482+ ),
483+ ) as executor :
484+ results = executor . map ( customize_manifest_file , filepaths , chunksize = 8 )
485+ for completed , _ in enumerate ( results , start = 1 ):
486+ if completed % 1000 == 0 or completed == len ( filepaths ):
487+ print (
488+ f"Customized { completed } / { len ( filepaths ) } HTML files" ,
489+ flush = True ,
490+ )
0 commit comments