1414from pathlib import Path
1515import shutil
1616import sys
17+ import inspect
18+ import os
1719
1820import mmif
1921
2426# -- Project information -----------------------------------------------------
2527
2628project = proj_root_dir .name
29+ blob_base_url = f'https://github.com/clamsproject/{ project } /blob'
2730copyright = f'{ datetime .date .today ().year } , Brandeis LLC'
2831author = 'Brandeis LLC'
29- version = open (proj_root_dir / 'VERSION' ).read ().strip ()
32+ try :
33+ version = open (proj_root_dir / 'VERSION' ).read ().strip ()
34+ except FileNotFoundError :
35+ print ("WARNING: VERSION file not found, using 'dev' as version." )
36+ version = 'dev'
3037root_doc = 'index'
3138
3239
3946 'sphinx.ext.autodoc' ,
4047 'sphinx.ext.linkcode' ,
4148 'sphinx.ext.intersphinx' ,
42- 'sphinx_rtd_theme' ,
4349 'sphinx-jsonschema' ,
4450 'm2r2'
4551]
5359 }
5460
5561
56- # Add any paths that contain templates here, relative to this directory.
5762templates_path = ['_templates' ]
58-
59- # List of patterns, relative to source directory, that match files and
60- # directories to ignore when looking for source files.
61- # This pattern also affects html_static_path and html_extra_path.
6263exclude_patterns = ['_build' , 'Thumbs.db' , '.DS_Store' ]
64+ # dynamically generated files
65+ exclude_patterns .extend (['cli_help.rst' , 'whatsnew.md' ])
6366
6467
6568# -- Options for HTML output -------------------------------------------------
6669
6770# The theme to use for HTML and HTML Help pages. See the documentation for
6871# a list of builtin themes.
6972#
70- html_theme = 'sphinx_rtd_theme'
71-
72- # Add any paths that contain custom static files (such as style sheets) here,
73- # relative to this directory. They are copied after the builtin static files,
74- # so a file named "default.css" will overwrite the builtin "default.css".
75- # html_static_path = ['_static']
76-
77- # hide document source view link at the top
78- html_show_sourcelink = False
73+ html_theme = 'furo'
74+ html_extra_path = ['appmetadata.jsonschema' ]
75+ html_static_path = [] # No static path for now, can be created if needed
76+ html_show_sourcelink = True # Furo handles this well, no need to hide
77+
78+ # Theme options for visual consistency with CLAMS branding
79+ html_theme_options = {
80+ # "light_logo": "logo.png", # TODO: Add logo files if available
81+ # "dark_logo": "logo.png",
82+ "sidebar_hide_name" : False ,
83+ "navigation_with_keys" : True ,
84+ "source_repository" : "https://github.com/clamsproject/clams-python" ,
85+ "source_branch" : "main" , # Default branch for "Edit on GitHub" links
86+ "source_directory" : "documentation/" ,
87+
88+ # CLAMS brand colors
89+ "light_css_variables" : {
90+ "color-brand-primary" : "#008AFF" ,
91+ "color-brand-content" : "#0085A1" ,
92+ "color-link" : "#008AFF" ,
93+ "color-link-hover" : "#0085A1" ,
94+ },
95+ # Dark mode variables can be added here if needed
96+ }
7997
8098
8199# function used by `linkcode` extension
82100def linkcode_resolve (domain , info ):
83- if domain != 'py' :
101+ if domain != 'py' or not info . get ( 'module' ) :
84102 return None
85- if not info ['module' ]:
103+
104+ try :
105+ # Find the Python object
106+ obj = sys .modules .get (info ['module' ])
107+ if obj is None : return None
108+ for part in info ['fullname' ].split ('.' ):
109+ obj = getattr (obj , part )
110+
111+ # Get the source file and line numbers
112+ # Use inspect.unwrap to handle decorated objects
113+ unwrapped_obj = inspect .unwrap (obj )
114+ filename = inspect .getsourcefile (unwrapped_obj )
115+ if not filename : return None
116+
117+ lines , start_lineno = inspect .getsourcelines (unwrapped_obj )
118+ end_lineno = start_lineno + len (lines ) - 1
119+
120+ # clams-python docs are single-version, always pointing to main
121+ git_ref = 'main'
122+
123+ # Get file path relative to repository root
124+ repo_root = Path (__file__ ).parent .parent
125+ rel_path = Path (filename ).relative_to (repo_root )
126+
127+ return f"{ blob_base_url } /{ git_ref } /{ rel_path } #L{ start_lineno } -L{ end_lineno } "
128+
129+ except Exception :
130+ # Don't fail the entire build if one link fails, just return None
86131 return None
87- filename = info ['module' ].replace ('.' , '/' )
88- return f"https://github.com/clamsproject/clams-python/tree/main/{ filename } /__init__.py"
89132
90133
91- def update_target_spec ():
134+ def generate_whatsnew_rst (app ):
135+ changelog_path = proj_root_dir / 'CHANGELOG.md'
136+ output_path = proj_root_dir / 'documentation' / 'whatsnew.md'
137+ if not changelog_path .exists ():
138+ print (f"WARNING: CHANGELOG.md not found at { changelog_path } " )
139+ with open (output_path , 'w' ) as f :
140+ f .write ("" )
141+ return
142+
143+ import re
144+
145+ content = []
146+ found_version = False
147+ version_header_re = re .compile (r'^## releasing\s+([^\s]+)\s*(\(.*\))?' )
148+
149+ print (f"DEBUG: Looking for version '{ version } ' in CHANGELOG.md" )
150+
151+ with open (changelog_path , 'r' ) as f :
152+ lines = f .readlines ()
153+
154+ for line in lines :
155+ match = version_header_re .match (line )
156+ if match :
157+ header_version = match .group (1 )
158+ if header_version == version :
159+ found_version = True
160+ # We don't include the header line itself in the content we want to wrap
161+ continue
162+ elif found_version :
163+ break
164+
165+ if found_version :
166+ content .append (line )
167+
168+ if not found_version :
169+ print (f"NOTE: No changelog entry found for version { version } " )
170+ with open (output_path , 'w' ) as f :
171+ f .write ("" )
172+ else :
173+ # Dump matched markdown content directly to whatsnew.md
174+ with open (output_path , 'w' ) as f :
175+ f .write (f"## What's New in { version } \n \n (Full changelog available in the [CHANGELOG.md]({ blob_base_url } /main/CHANGELOG.md))\n " )
176+ f .writelines (content )
177+
178+
179+ def generate_jsonschema (app ):
180+ import json
181+ from clams .appmetadata import AppMetadata
182+
183+ # Generate schema using Pydantic v2 API
184+ schema_dict = AppMetadata .model_json_schema ()
185+
186+ output_path = Path (app .srcdir ) / 'appmetadata.jsonschema'
187+ with open (output_path , 'w' ) as f :
188+ json .dump (schema_dict , f , indent = 2 )
189+
190+
191+ def update_target_spec (app ):
92192 target_vers_csv = Path (__file__ ).parent / 'target-versions.csv'
93- with open ("../ VERSION" , 'r' ) as version_f :
193+ with open (proj_root_dir / " VERSION" , 'r' ) as version_f :
94194 version = version_f .read ().strip ()
95195 mmifver = mmif .__version__
96196 specver = mmif .__specver__
@@ -102,4 +202,8 @@ def update_target_spec():
102202 out_f .write (line )
103203 shutil .move (out_f .name , in_f .name )
104204
105- update_target_spec ()
205+
206+ def setup (app ):
207+ app .connect ('builder-inited' , generate_whatsnew_rst )
208+ app .connect ('builder-inited' , generate_jsonschema )
209+ app .connect ('builder-inited' , update_target_spec )
0 commit comments