1313
1414import m2r
1515import os
16+ import re
17+ import semver
1618import sys
1719
1820from CPAC import __version__
2123from github .GithubException import RateLimitExceededException , \
2224 UnknownObjectException
2325
26+ # "Dealing with Invalid Versions" from
27+ # https://python-semver.readthedocs.io/en/latest/usage.html
28+
29+
30+ def coerce (version ):
31+ """
32+ Convert an incomplete version string into a semver-compatible VersionInfo
33+ object
34+
35+ * Tries to detect a "basic" version string (``major.minor.patch``).
36+ * If not enough components can be found, missing components are
37+ set to zero to obtain a valid semver version.
38+
39+ :param str version: the version string to convert
40+ :return: a tuple with a :class:`VersionInfo` instance (or ``None``
41+ if it's not a version) and the rest of the string which doesn't
42+ belong to a basic version.
43+ :rtype: tuple(:class:`VersionInfo` | None, str)
44+ """
45+ BASEVERSION = re .compile (
46+ r"""[vV]?
47+ (?P<major>0|[1-9]\d*)
48+ (\.
49+ (?P<minor>0|[1-9]\d*)
50+ (\.
51+ (?P<patch>0|[1-9]\d*)
52+ )?
53+ )?
54+ """ ,
55+ re .VERBOSE ,
56+ )
57+
58+ match = BASEVERSION .search (version )
59+ if not match :
60+ return (None , version )
61+
62+ ver = {
63+ key : 0 if value is None else value for key , value in match .groupdict ().items ()
64+ }
65+ ver = semver .VersionInfo (** ver )
66+ rest = match .string [match .end () :] # noqa:E203
67+ return ver , rest
68+
69+
70+ def compare_versions (new , old ):
71+ """
72+ Function to compare two versions.
73+
74+ Parameters
75+ ----------
76+ new: str
77+
78+ old: str
79+
80+ Returns
81+ -------
82+ bool
83+ Is the "new" at least as new as "old"?
84+ """
85+ comparisons = list (zip (coerce (new ), coerce (old )))
86+ if any ([v is None for v in comparisons [0 ]]):
87+ return (False )
88+ outright = semver .compare (str (comparisons [0 ][0 ]), str (comparisons [0 ][1 ]))
89+ return (
90+ bool (outright == 1 ) or bool (
91+ (outright == 0 ) and comparisons [1 ][0 ] >= comparisons [1 ][1 ]
92+ )
93+ )
94+
95+
2496# If extensions (or modules to document with autodoc) are in another directory,
2597# add these directories to sys.path here. If the directory is relative to the
2698# documentation root, use os.path.abspath to make it absolute, like shown here.
74146
75147# Get tags from GitHub
76148# Set GITHUBTOKEN to your API token in your environment to increase rate limit.
77- g = Github (os .environ .get (" GITHUBTOKEN" ))
149+ g = Github (os .environ .get (' GITHUBTOKEN' ))
78150
79151def _gh_rate_limit ():
80152 print ("""Release notes not updated due to GitHub API rate limit.
@@ -100,21 +172,32 @@ def _gh_rate_limit():
100172""" )
101173
102174try :
103- gh_cpac = g .get_user (" FCP-INDI" ).get_repo (" C-PAC" )
175+ gh_cpac = g .get_user (' FCP-INDI' ).get_repo (' C-PAC' )
104176 gh_tags = [t .name for t in gh_cpac .get_tags ()]
105177except RateLimitExceededException :
106178 _gh_rate_limit ()
107179 gh_tags = []
108180gh_tags .sort (reverse = True )
109181
182+ build_version_path = os .path .abspath (os .path .join (
183+ __file__ , os .pardir , os .pardir , os .pardir , 'build_version.txt'
184+ ))
185+ # don't build release notes for newer releases
186+ if os .path .exists (build_version_path ):
187+ with open (build_version_path , 'r' ) as bvf :
188+ build_version = bvf .read ().strip ()
189+ gh_tags = [gh_tag for gh_tag in gh_tags if compare_versions (
190+ build_version , gh_tag
191+ )]
192+
110193# Try to get release notes from GitHub
111194try :
112195 gh_releases = []
113196 for t in gh_tags :
114197 try :
115198 gh_releases .append (gh_cpac .get_release (t ).raw_data )
116199 except (AttributeError , UnknownObjectException ):
117- print (f" No notes for { t } " )
200+ print (f' No notes for { t } ' )
118201 gh_releaseNotes = {r ['tag_name' ]: {
119202 'name' : r ['name' ],
120203 'body' : r ['body' ],
@@ -124,10 +207,13 @@ def _gh_rate_limit():
124207 _gh_rate_limit ()
125208 gh_releaseNotes = {
126209 t : {
127- "name" : t ,
128- "body" : f"See https://github.com/FCP-INDI/C-PAC/releases/tag/{ t } for "
129- "release notes." ,
130- "published_at" : None
210+ 'name' : t ,
211+ 'body' : '' .join ([
212+ 'See https://github.com/FCP-INDI/C-PAC/releases/tag/' ,
213+ t ,
214+ ' for release notes.'
215+ ]),
216+ 'published_at' : None
131217 } for t in gh_tags
132218 }
133219
@@ -141,66 +227,66 @@ def _unireplace(release_note, unireplace):
141227 e2 = str (e [2 :])
142228 release_note = release_note .replace (
143229 e ,
144- f" |u{ e2 } | "
230+ f' |u{ e2 } | '
145231 )
146232 unireplace [e2 ] = e
147233 return (_unireplace (release_note , unireplace ))
148234 return (
149235 release_note ,
150- " \n \n " .join ([
151- f" .. |u{ u } | unicode:: { v } "
236+ ' \n \n ' .join ([
237+ f' .. |u{ u } | unicode:: { v } '
152238 for u , v in list (unireplace .items ())])
153239 )
154240
155241this_dir = os .path .dirname (os .path .abspath (__file__ ))
156- release_notes_dir = os .path .join (this_dir , " user" , " release_notes" )
242+ release_notes_dir = os .path .join (this_dir , ' user' , ' release_notes' )
157243if not os .path .exists (release_notes_dir ):
158244 os .makedirs (release_notes_dir )
159- latest_path = os .path .join (release_notes_dir , " latest.rst" )
245+ latest_path = os .path .join (release_notes_dir , ' latest.rst' )
160246# all_release_notes = ""
161247for t in gh_tags :
162248 if t in gh_releaseNotes :
163- tag_header = " {}{}{}" .format (
164- " Latest Release: " if t == gh_tags [0 ] else "" ,
249+ tag_header = ' {}{}{}' .format (
250+ ' Latest Release: ' if t == gh_tags [0 ] else '' ,
165251 (
166252 gh_releaseNotes [t ]['name' ][4 :] if (
167- gh_releaseNotes [t ]['name' ].startswith (" CPAC" )
253+ gh_releaseNotes [t ]['name' ].startswith (' CPAC' )
168254 ) else gh_releaseNotes [t ]['name' ][5 :] if (
169- gh_releaseNotes [t ]['name' ].startswith (" C-PAC" )
255+ gh_releaseNotes [t ]['name' ].startswith (' C-PAC' )
170256 ) else gh_releaseNotes [t ]['name' ]
171257 ).strip (),
172- " ({})" .format (
258+ ' ({})' .format (
173259 dparser .parse (gh_releaseNotes [t ]['published_at' ]).date (
174- ).strftime (" %b %w , %Y" )
175- ) if gh_releaseNotes [t ]['published_at' ] else ""
260+ ).strftime (' %b %d , %Y' )
261+ ) if gh_releaseNotes [t ]['published_at' ] else ''
176262 )
177- release_note = " \n " .join (_unireplace (
263+ release_note = ' \n ' .join (_unireplace (
178264 """{}
179265{}
180266{}
181267""" .format (
182268 tag_header ,
183- "^" * len (tag_header ),
269+ '^' * len (tag_header ),
184270 m2r .convert (gh_releaseNotes [t ]['body' ].encode (
185- " ascii" ,
186- errors = " backslashreplace"
187- ).decode (" utf-8" ))
271+ ' ascii' ,
272+ errors = ' backslashreplace'
273+ ).decode (' utf-8' ))
188274 ),
189275 {}
190276 ))
191277
192- release_notes_path = os .path .join (release_notes_dir , "{ }.rst" . format ( t ) )
278+ release_notes_path = os .path .join (release_notes_dir , f' { t } .rst' )
193279 if gh_releaseNotes [t ]['published_at' ] and not os .path .exists (
194280 release_notes_path
195281 ) and not os .path .exists (
196- os .path .join (release_notes_dir , "v{ }.rst" . format ( t ) )
282+ os .path .join (release_notes_dir , f'v { t } .rst' )
197283 ):
198284 with open (release_notes_path , 'w+' ) as f :
199285 f .write (release_note )
200286 else :
201287 print (release_notes_path )
202288
203- if tag_header .startswith (" Latest" ) and not os .path .exists (latest_path ):
289+ if tag_header .startswith (' Latest' ) and not os .path .exists (latest_path ):
204290 with open (latest_path , 'w+' ) as f :
205291 f .write (
206292 """
@@ -216,8 +302,8 @@ def _unireplace(release_note, unireplace):
216302
217303rnd = [
218304 d for d in os .listdir (release_notes_dir ) if d not in [
219- " index.rst" ,
220- " latest.rst"
305+ ' index.rst' ,
306+ ' latest.rst'
221307 ]
222308]
223309rnd .sort (key = sort_tag , reverse = True )
@@ -231,18 +317,18 @@ def _unireplace(release_note, unireplace):
231317 {}
232318
233319""" .format (
234- " \n " .join ([
235- " .. include:: /user/release_notes/{}" . format ( fp ) for fp in rnd
320+ ' \n ' .join ([
321+ f' .. include:: /user/release_notes/{ fp } ' for fp in rnd
236322 ]),
237- " \n " .join ([
238- " /user/release_notes/{}" . format ( d ) for d in rnd
323+ ' \n ' .join ([
324+ f' /user/release_notes/{ d } ' for d in rnd
239325]))
240326with open (os .path .join (release_notes_dir , 'index.rst' ), 'w+' ) as f :
241327 f .write (all_release_notes .strip ())
242328
243329
244330# The full version, including alpha/beta/rc tags.
245- release = '{ } Beta'. format ( __version__ )
331+ release = f' { __version__ } Beta'
246332
247333# The language for content autogenerated by Sphinx. Refer to documentation
248334# for a list of supported languages.
@@ -256,7 +342,7 @@ def _unireplace(release_note, unireplace):
256342
257343# List of patterns, relative to source directory, that match files and
258344# directories to ignore when looking for source files.
259- exclude_patterns = [" futuredocs/*" ]
345+ exclude_patterns = [' futuredocs/*' ]
260346
261347# The reST default role (used for this markup: `text`) to use for all documents.
262348#default_role = None
@@ -289,40 +375,40 @@ def _unireplace(release_note, unireplace):
289375# further. For a list of options available for each theme, see the
290376# documentation.
291377html_theme_options = {
292- " relbarbgcolor" : " #0067a0" ,
293- " sidebarbgcolor" : " #f0f0f0" ,
294- " sidebartextcolor" : " #000000" ,
295- " sidebarlinkcolor" : " #0067a0" ,
296- " headbgcolor" : " #919d9d" ,
297- " headtextcolor" : " #e4e4e4"
378+ ' relbarbgcolor' : ' #0067a0' ,
379+ ' sidebarbgcolor' : ' #f0f0f0' ,
380+ ' sidebartextcolor' : ' #000000' ,
381+ ' sidebarlinkcolor' : ' #0067a0' ,
382+ ' headbgcolor' : ' #919d9d' ,
383+ ' headtextcolor' : ' #e4e4e4'
298384}
299385
300386# Add any paths that contain custom themes here, relative to this directory.
301- html_theme_path = [" ../Themes" ]
387+ html_theme_path = [' ../Themes' ]
302388
303389html_css_files = [
304390 'custom.css' ,
305391]
306392
307393# The name for this set of Sphinx documents. If None, it defaults to
308- # " <project> v<release> documentation" .
394+ # ' <project> v<release> documentation' .
309395#html_title = None
310396
311397# A shorter title for the navigation bar. Default is the same as html_title.
312398#html_short_title = None
313399
314400# The name of an image file (relative to this directory) to place at the top
315401# of the sidebar.
316- html_logo = " _static/cpac_logo_vertical.png"
402+ html_logo = ' _static/cpac_logo_vertical.png'
317403
318404# The name of an image file (within the static path) to use as favicon of the
319405# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
320406# pixels large.
321- html_favicon = " favicon.ico"
407+ html_favicon = ' favicon.ico'
322408
323409# Add any paths that contain custom static files (such as style sheets) here,
324410# relative to this directory. They are copied after the builtin static files,
325- # so a file named " default.css" will overwrite the builtin " default.css" .
411+ # so a file named ' default.css' will overwrite the builtin ' default.css' .
326412html_static_path = ['_static' ]
327413
328414# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
@@ -358,23 +444,23 @@ def _unireplace(release_note, unireplace):
358444# If true, links to the reST sources are added to the pages.
359445#html_show_sourcelink = True
360446
361- # If true, " Created using Sphinx" is shown in the HTML footer. Default is True.
447+ # If true, ' Created using Sphinx' is shown in the HTML footer. Default is True.
362448#html_show_sphinx = True
363449
364- # If true, " (C) Copyright ..." is shown in the HTML footer. Default is True.
450+ # If true, ' (C) Copyright ...' is shown in the HTML footer. Default is True.
365451#html_show_copyright = True
366452
367453# If true, an OpenSearch description file will be output, and all pages will
368454# contain a <link> tag referring to it. The value of this option must be the
369455# base URL from which the finished HTML is served.
370456#html_use_opensearch = ''
371457
372- # This is the file name suffix for HTML files (e.g. " .xhtml" ).
373- html_file_suffix = " .html"
458+ # This is the file name suffix for HTML files (e.g. ' .xhtml' ).
459+ html_file_suffix = ' .html'
374460
375461# Suffix for generated links to HTML files
376- html_link_suffix = ""
377- link_suffix = ""
462+ html_link_suffix = ''
463+ link_suffix = ''
378464
379465# Output file base name for HTML help builder.
380466htmlhelp_basename = 'C-PACdoc'
@@ -404,7 +490,7 @@ def _unireplace(release_note, unireplace):
404490# the title page.
405491#latex_logo = None
406492
407- # For " manual" documents, if this is true, then toplevel headings are parts,
493+ # For ' manual' documents, if this is true, then toplevel headings are parts,
408494# not chapters.
409495#latex_use_parts = False
410496
@@ -461,3 +547,4 @@ def _unireplace(release_note, unireplace):
461547""" .format (
462548 versions = ", " .join (gh_tags [:5 ])
463549)
550+
0 commit comments