#!/usr/bin/env python3 """ Enhanced documentation generator for the mrdna package using MkDocs with Material theme. This script: 1. Scans the mrdna package structure 2. Generates markdown documentation files 3. Creates a navigable documentation website using MkDocs with Material theme 4. Adds support for API reference documentation through mkdocstrings """ import os import sys import inspect import importlib import subprocess from pathlib import Path import shutil import yaml import re from textwrap import dedent from collections import defaultdict # Configure documentation paths DOCS_DIR = Path("docs") SITE_DIR = Path("site") CONFIG_FILE = Path("mkdocs.yml") # Core modules to document MODULES_TO_DOCUMENT = { "core": [ "mrdna.config", "mrdna.coords", "mrdna.reporting", "mrdna.segmentmodel", "mrdna.simulate", "mrdna.version" ], "model": [ "mrdna.model.CanonicalNucleotideAtoms", "mrdna.model.dna_sequence", "mrdna.model.nbPot", "mrdna.model.spring_from_lp" ], "readers": [ "mrdna.readers.cadnano_segments", "mrdna.readers.polygon_mesh", "mrdna.readers.segmentmodel_from_cadnano", "mrdna.readers.segmentmodel_from_lists", "mrdna.readers.segmentmodel_from_oxdna", "mrdna.readers.segmentmodel_from_pdb", "mrdna.readers.segmentmodel_from_scadnano" ], "arbdmodel": [ "mrdna.arbdmodel.coords", "mrdna.arbdmodel.grid", "mrdna.arbdmodel.interactions", "mrdna.arbdmodel.polymer" ] } def check_command_exists(command): """Check if a command exists in the system path.""" try: subprocess.run([command, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) return True except FileNotFoundError: return False def extract_module_info(module_name): """Extract useful information from a module.""" try: module = importlib.import_module(module_name) docstring = module.__doc__ or "" # If the module has a __version__ attribute, include it version = getattr(module, "__version__", None) # Extract classes classes = {} for name, obj in inspect.getmembers(module, inspect.isclass): if obj.__module__ == module_name: classes[name] = { "docstring": obj.__doc__ or "", "methods": {} } # Extract methods for method_name, method in inspect.getmembers(obj, inspect.isfunction): if not method_name.startswith("_") or method_name == "__init__": classes[name]["methods"][method_name] = { "docstring": method.__doc__ or "", "signature": str(inspect.signature(method)) } # Extract functions functions = {} for name, obj in inspect.getmembers(module, inspect.isfunction): if obj.__module__ == module_name and not name.startswith("_"): functions[name] = { "docstring": obj.__doc__ or "", "signature": str(inspect.signature(obj)) } return { "docstring": docstring, "version": version, "classes": classes, "functions": functions, "module_name": module_name } except (ImportError, AttributeError) as e: print(f"Error extracting info from {module_name}: {e}") return { "docstring": f"Module documentation for {module_name}. (Error: {e})", "version": None, "classes": {}, "functions": {}, "module_name": module_name } def create_module_md(module_name, docs_dir): """Create Markdown documentation for a Python module.""" try: # Extract module name and subpackage for organization parts = module_name.split('.') simple_name = parts[-1] subpackage = parts[1] if len(parts) > 2 else 'core' # Get module information module_info = extract_module_info(module_name) # Start building the Markdown content content = [ f"# {simple_name}", "", ] # Add module docstring if module_info["docstring"]: content.append(module_info["docstring"].strip()) content.append("") else: content.append(f"Module documentation for {simple_name}.") content.append("") # Add version if available if module_info["version"]: content.append(f"**Version:** {module_info['version']}") content.append("") # Add mkdocstrings directive for API docs content.extend([ "## API Reference", "", f"::: {module_name}", " handler: python", " selection:", " docstring_style: google", " rendering:", " show_source: true", "" ]) # Create directory for module documentation output_dir = docs_dir / "api" / subpackage output_dir.mkdir(exist_ok=True, parents=True) # Write the Markdown file output_path = output_dir / f"{simple_name}.md" with open(output_path, 'w') as f: f.write('\n'.join(content)) print(f"Created module documentation: {output_path}") return output_path.relative_to(docs_dir), simple_name except Exception as e: print(f"Error processing module {module_name}: {e}") return None, None def create_subpackage_index(subpackage, module_names, docs_dir): """Create an index Markdown file for a subpackage.""" # Create index content subpackage_title = subpackage.capitalize() content = [ f"# {subpackage_title} Modules", "", f"This section covers the modules in the {subpackage} subpackage.", "", "## Modules", "", ] # Process each module in the subpackage module_info = [] for module_name in sorted(module_names): rel_path, simple_name = create_module_md(module_name, docs_dir) if rel_path and simple_name: full_module_name = module_name brief_desc = "" try: mod = importlib.import_module(module_name) if mod.__doc__: brief_desc = mod.__doc__.strip().split('\n')[0] except: pass module_info.append((rel_path, simple_name, full_module_name, brief_desc)) # If no modules were successfully processed, skip creating the index if not module_info: print(f"No modules were successfully processed for {subpackage}, skipping index") return None # Add module links to index with descriptions for rel_path, simple_name, full_module_name, brief_desc in sorted(module_info, key=lambda x: x[1]): # Fix the relative path to ensure it's correct from the packages directory # This ensures links are properly resolved by MkDocs fixed_path = f"../{rel_path}" content.append(f"- [{simple_name}]({fixed_path}): {brief_desc}") # Create directory for subpackage documentation output_dir = docs_dir / "packages" output_dir.mkdir(exist_ok=True, parents=True) # Write the index file index_path = output_dir / f"{subpackage}.md" with open(index_path, 'w') as f: f.write('\n'.join(content)) print(f"Created subpackage index: {index_path}") return index_path.relative_to(docs_dir) def create_main_index(subpackages, docs_dir): """Create the main index.md file.""" content = [ "# mrDNA Documentation", "", "Welcome to the documentation for mrDNA!", "", "mrDNA is a molecular dynamics package for DNA modeling and simulation.", "", "## Package Structure", "", "The package is organized into the following subpackages:", "", ] # Add each subpackage to the list for subpackage in sorted(subpackages): subpackage_title = subpackage.capitalize() content.append(f"- [{subpackage_title}](packages/{subpackage}.md)") # Write the index file index_path = docs_dir / "index.md" with open(index_path, 'w') as f: f.write('\n'.join(content)) print(f"Created main index: {index_path}") def create_installation_page(docs_dir): """Create the installation page.""" with open(Path("README.md"), 'r') as f: readme_content = f.read() # Extract installation instructions from README installation_match = re.search(r'## Installation(.*?)(?:##|\Z)', readme_content, re.DOTALL) if installation_match: installation_content = installation_match.group(1).strip() else: installation_content = """ Please refer to the README file for installation instructions. """ # Create a more detailed installation guide content = [ "# Installation", "", "## Requirements", "", "mrDNA requires the following dependencies:", "", "- Python 3.6+", "- NumPy", "- SciPy", "- MDAnalysis (for certain functionality)", "- CUDA Toolkit (for GPU acceleration)", "- ARBD simulation engine", "", "## Installation Instructions", "", installation_content, "", "## Verifying Installation", "", "After installation, you can verify that mrDNA is working correctly by running a simple test:", "", "```python", "import mrdna", "print(mrdna.__version__)", "```", "", "This should print the version number of the installed mrDNA package.", ] # Write the installation file install_path = docs_dir / "installation.md" with open(install_path, 'w') as f: f.write('\n'.join(content)) print(f"Created installation page: {install_path}") def create_getting_started(docs_dir): """Create the getting started page.""" content = [ "# Getting Started", "", "This guide will help you get started with using mrDNA for DNA molecular modeling.", "", "## Basic Usage", "", "Here's a simple example that creates a DNA structure:", "", "```python", "# Import necessary modules", "from mrdna.segmentmodel import SegmentModel", "from mrdna.model.dna_sequence import DNASequence", "", "# Create a segment model", "model = SegmentModel()", "", "# Add a DNA segment with a specific sequence", "sequence = \"GATTACA\"", "model.add_segment(sequence)", "", "# Save the model to a PDB file", "model.to_pdb(\"example.pdb\")", "```", "", "## Working with Existing Structures", "", "mrDNA can also read structures from various file formats:", "", "```python", "# Import the appropriate reader", "from mrdna.readers import read_cadnano", "", "# Load a structure from a cadnano file", "model = read_cadnano(\"my_structure.json\")", "", "# Analyze the structure", "num_nucleotides = model.count_nucleotides()", "print(f\"Structure contains {num_nucleotides} nucleotides\")", "```", "", "## Running a Simulation", "", "To run a simulation with mrDNA:", "", "```python", "from mrdna.simulate import multiresolution_simulation", "", "# Set up simulation parameters", "output_name = \"my_simulation\"", "gpu = 0 # GPU device to use", "", "# Run the simulation", "result_directory = multiresolution_simulation(", " model,", " output_name=output_name,", " gpu=gpu,", " coarse_steps=1e7, # Number of coarse-grained steps", " fine_steps=1e7, # Number of fine-grained steps", ")", "```", "", "## Next Steps", "", "Explore the API reference documentation for more detailed information on each module and function.", ] # Write the getting started file getting_started_path = docs_dir / "getting_started.md" with open(getting_started_path, 'w') as f: f.write('\n'.join(content)) print(f"Created getting started page: {getting_started_path}") def create_tutorials(docs_dir): """Create tutorials directory with example files.""" tutorials_dir = docs_dir / "tutorials" tutorials_dir.mkdir(exist_ok=True) # Create a basic tutorial basic_tutorial = [ "# Basic DNA Origami Tutorial", "", "This tutorial walks through the process of creating a simple DNA origami structure using mrDNA.", "", "## Loading a design from cadnano", "", "```python", "from mrdna.readers import read_cadnano", "", "# Load a cadnano design", "model = read_cadnano('6hb.json')", "```", "", "## Visualizing the structure", "", "You can generate PDB files to visualize the structure in molecular viewers like VMD or PyMOL:", "", "```python", "# Create a coarse-grained model", "model.generate_bead_model(max_basepairs_per_bead=5, max_nucleotides_per_bead=5)", "", "# Output PDB files", "model.write_pdb('6hb_cg.pdb')", "```", "", "## Running a simulation", "", "```python", "from mrdna.simulate import multiresolution_simulation", "", "# Run a multi-resolution simulation", "result_dir = multiresolution_simulation(", " model, ", " output_name='6hb',", " gpu=0,", " coarse_steps=5e7,", " fine_steps=5e7", ")", "```", ] with open(tutorials_dir / "basic_origami.md", 'w') as f: f.write('\n'.join(basic_tutorial)) # Create a tutorial index tutorial_index = [ "# Tutorials", "", "This section contains tutorials for using mrDNA for different applications.", "", "## Available Tutorials", "", "- [Basic DNA Origami](basic_origami.md): Create and simulate a simple DNA origami structure", ] with open(tutorials_dir / "index.md", 'w') as f: f.write('\n'.join(tutorial_index)) print(f"Created tutorials directory: {tutorials_dir}") def create_mkdocs_config(subpackages): """Create the MkDocs configuration file.""" nav = [ {"Home": "index.md"}, {"Installation": "installation.md"}, {"Getting Started": "getting_started.md"}, {"Tutorials": "tutorials/index.md"}, ] # Add API reference to navigation api_nav = [] for subpackage in sorted(subpackages): subpackage_title = subpackage.capitalize() api_nav.append({subpackage_title: f"packages/{subpackage}.md"}) nav.append({"API Reference": api_nav}) # Create the configuration config = { "site_name": "mrDNA Documentation", "site_description": "Documentation for the mrDNA DNA molecular modeling package", "site_author": "Chris Maffeo", "repo_url": "https://gitlab.engr.illinois.edu/tbgl/tools/mrdna", "theme": { "name": "material", "palette": { "primary": "blue grey", "accent": "light blue" }, "features": [ "navigation.instant", "navigation.tracking", "navigation.expand", "navigation.indexes", "navigation.top", "search.highlight", "search.share", "content.code.copy" ], "icon": { "repo": "fontawesome/brands/gitlab" } }, "markdown_extensions": [ "pymdownx.highlight", "pymdownx.superfences", "pymdownx.tabbed", "pymdownx.arithmatex", "admonition", "footnotes", "attr_list", "pymdownx.details", "pymdownx.emoji", "toc" ], "plugins": [ "search", "mkdocstrings", { "autorefs": {} } ], "nav": nav, "extra": { "social": [ { "icon": "fontawesome/brands/gitlab", "link": "https://gitlab.engr.illinois.edu/tbgl/tools/mrdna", "name": "mrDNA on GitLab" } ] } } # Write the configuration file with open(CONFIG_FILE, 'w') as f: yaml.dump(config, f, default_flow_style=False, sort_keys=False) print(f"Created MkDocs configuration file: {CONFIG_FILE}") def build_docs(): """Build the documentation using MkDocs.""" try: print("\nBuilding documentation...") subprocess.run(['mkdocs', 'build'], check=True) print(f"\nDocumentation successfully built. You can view it at: {SITE_DIR / 'index.html'}") return True except subprocess.CalledProcessError as e: print(f"Error building documentation: {e}") return False except FileNotFoundError: print("Could not find mkdocs. Make sure MkDocs is installed and in your PATH.") return False def check_dependencies(): """Check if required dependencies are installed.""" missing_deps = [] if not check_command_exists("mkdocs"): missing_deps.append("mkdocs") # Check for mkdocstrings - try multiple ways try: import mkdocstrings except ImportError: try: # Try alternative import path subprocess.run([sys.executable, "-c", "import mkdocstrings"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except: missing_deps.append("mkdocstrings") # Check for mkdocs-material try: # First try direct import import material except ImportError: try: # Alternative check: try to import the theme module subprocess.run([sys.executable, "-c", "import mkdocs.themes.material"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except: # Another way to check if a theme is installed try: # Check if mkdocs recognizes the theme result = subprocess.run(["mkdocs", "build", "--theme", "material", "--dry-run"], check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode != 0: missing_deps.append("mkdocs-material") except: missing_deps.append("mkdocs-material") if missing_deps: print("Missing dependencies: " + ", ".join(missing_deps)) print("\nPlease install the required dependencies with:") print(f"pip install {' '.join(missing_deps)}") print("\nFor better API documentation, also install:") print("pip install \"mkdocstrings[python]\"") response = input("\nDo you want to continue anyway? (y/n): ") if response.lower() in ('y', 'yes'): return True return False return True def main(): """Generate documentation for the mrdna package using MkDocs.""" # Check dependencies if not check_dependencies(): print("\nProceeding with a warning: Some dependencies may be missing.") print("The documentation generation might not complete successfully.") # Create or clean docs directory if DOCS_DIR.exists(): shutil.rmtree(DOCS_DIR) DOCS_DIR.mkdir() # Generate documentation for each subpackage in the predefined list for subpackage, module_list in MODULES_TO_DOCUMENT.items(): create_subpackage_index(subpackage, module_list, DOCS_DIR) # Create main index and additional pages create_main_index(MODULES_TO_DOCUMENT.keys(), DOCS_DIR) create_installation_page(DOCS_DIR) create_getting_started(DOCS_DIR) create_tutorials(DOCS_DIR) # Create MkDocs configuration create_mkdocs_config(MODULES_TO_DOCUMENT.keys()) print(f"\nDocumentation files generated in {DOCS_DIR}") # Build the documentation response = input("\nWould you like to build the HTML documentation now? (y/n): ") if response.lower() in ('y', 'yes'): success = build_docs() if success: print("\nTo preview the documentation with a local server, run:") print("mkdocs serve") else: print("\nTo build the documentation later, run:") print("mkdocs build") print("\nTo preview the documentation with a local server, run:") print("mkdocs serve") if __name__ == "__main__": main()