File: markdown_release_notes.py

package info (click to toggle)
nibabel 5.3.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 69,232 kB
  • sloc: python: 39,851; makefile: 276; ansic: 202; sh: 99
file content (140 lines) | stat: -rw-r--r-- 4,428 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/usr/bin/env python
import re
import sys
from collections import defaultdict
from functools import cache
from operator import call
from pathlib import Path

from sphinx.ext.intersphinx import fetch_inventory

CHANGELOG = Path(__file__).parent.parent / 'Changelog'

# Match release lines like "5.2.0 (Monday 11 December 2023)"
RELEASE_REGEX = re.compile(r"""((?:\d+)\.(?:\d+)\.(?:\d+)) \(\w+ \d{1,2} \w+ \d{4}\)$""")


class MockConfig:
    intersphinx_timeout: int | None = None
    tls_verify = False
    tls_cacerts: str | dict[str, str] | None = None
    user_agent: str = ''


@call
class MockApp:
    srcdir = ''
    config = MockConfig()


fetch_inv = cache(fetch_inventory)


def get_intersphinx(obj):
    module = obj.split('.', 1)[0]

    registry = defaultdict(lambda: 'https://docs.python.org/3')
    registry.update(
        numpy='https://numpy.org/doc/stable',
    )

    base_url = registry[module]

    inventory = fetch_inv(MockApp, '', f'{base_url}/objects.inv')
    # Check py: first, then whatever
    for objclass in sorted(inventory, key=lambda x: not x.startswith('py:')):
        if obj in inventory[objclass]:
            return f'{base_url}/{inventory[objclass][obj][2]}'
    raise ValueError("Couldn't lookup {obj}")


def main():
    version = sys.argv[1]
    output = sys.argv[2]
    if output == '-':
        output = sys.stdout
    else:
        output = open(output, 'w')

    release_notes = []
    in_release_notes = False

    with open(CHANGELOG) as f:
        for line in f:
            match = RELEASE_REGEX.match(line)
            if match:
                if in_release_notes:
                    break
                in_release_notes = match.group(1) == version
                next(f)  # Skip the underline
                continue

            if in_release_notes:
                release_notes.append(line)

    # Drop empty lines at start and end
    while release_notes and not release_notes[0].strip():
        release_notes.pop(0)
    while release_notes and not release_notes[-1].strip():
        release_notes.pop()

    # Join lines
    release_notes = ''.join(release_notes)

    # Remove line breaks when they are followed by a space
    release_notes = re.sub(r'\n +', ' ', release_notes)

    # Replace pr/<number> with #<number> for GitHub
    release_notes = re.sub(r'pr/(\d+)', r'#\1', release_notes)

    # Replace :mod:`package.X` with [package.X](...)
    release_notes = re.sub(
        r':mod:`nibabel\.(.*)`',
        r'[nibabel.\1](https://nipy.org/nibabel/reference/nibabel.\1.html)',
        release_notes,
    )
    # Replace :class/func/attr:`package.module.X` with [package.module.X](...)
    release_notes = re.sub(
        r':(?:class|func|attr):`(nibabel\.\w*)(\.[\w.]*)?\.(\w+)`',
        r'[\1\2.\3](https://nipy.org/nibabel/reference/\1.html#\1\2.\3)',
        release_notes,
    )
    release_notes = re.sub(
        r':(?:class|func|attr):`~(nibabel\.\w*)(\.[\w.]*)?\.(\w+)`',
        r'[\3](https://nipy.org/nibabel/reference/\1.html#\1\2.\3)',
        release_notes,
    )
    # Replace :meth:`package.module.class.X` with [package.module.class.X](...)
    release_notes = re.sub(
        r':meth:`(nibabel\.[\w.]*)\.(\w+)\.(\w+)`',
        r'[\1.\2.\3](https://nipy.org/nibabel/reference/\1.html#\1.\2.\3)',
        release_notes,
    )
    release_notes = re.sub(
        r':meth:`~(nibabel\.[\w.]*)\.(\w+)\.(\w+)`',
        r'[\3](https://nipy.org/nibabel/reference/\1.html#\1.\2.\3)',
        release_notes,
    )
    # Replace :<any>:`<ref>` with intersphinx lookup
    for ref in re.findall(r'(:[^:]*:`~?\w[\w.]+\w`)', release_notes):
        objclass, tilde, module, obj = re.match(r':([^:]*):`(~?)([\w.]+)\.(\w+)`', ref).groups()
        url = get_intersphinx(f'{module}.{obj}')
        mdlink = f'[{"" if tilde else module}{obj}]({url})'
        release_notes = release_notes.replace(ref, mdlink)
    # Replace RST links with Markdown links
    release_notes = re.sub(r'`([^<`]*) <([^>]*)>`_+', r'[\1](\2)', release_notes)

    def python_doc(match):
        module = match.group(1)
        name = match.group(2)
        return f'[{name}](https://docs.python.org/3/library/{module.lower()}.html#{module}.{name})'

    release_notes = re.sub(r':meth:`~([\w.]+)\.(\w+)`', python_doc, release_notes)

    with output:
        output.write('## Release notes\n\n')
        output.write(release_notes)


if __name__ == '__main__':
    main()