File: presentation.py

package info (click to toggle)
python-releases 2.1.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 352 kB
  • sloc: python: 1,580; makefile: 11
file content (287 lines) | stat: -rw-r--r-- 10,659 bytes parent folder | download
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
from docutils.nodes import (
    reference,
    bullet_list,
    list_item,
    literal,
    raw,
    paragraph,
    Text,
)

from releases import Issue, construct_releases, construct_nodes

from _util import b, f, s, entry, make_app, release, releases, setup_issues


def _obj2name(obj):
    cls = obj if isinstance(obj, type) else obj.__class__
    return cls.__name__.split(".")[-1]


def _expect_type(node, cls):
    type_ = _obj2name(node)
    name = _obj2name(cls)
    msg = f"Expected {node!r} to be a {name}, but it's a {type_}"
    assert isinstance(node, cls), msg


class presentation:
    """
    Expansion/extension of docutils nodes (rendering)
    """

    def setup_method(self):
        setup_issues(self)

    def _generate(self, *entries, **kwargs):
        raw = kwargs.pop("raw", False)
        nodes = construct_nodes(releases(*entries, **kwargs))
        # By default, yield the contents of the bullet list.
        return nodes if raw else nodes[0][1][0]

    def _test_link(self, kwargs, type_, expected, entries=None):
        app = make_app(**kwargs)
        # Lazy-evaluated entries: (callable, *args)
        entries = entries or [
            (release, "1.0.2"),
            (b, 15),
            (release, "1.0.0"),
        ]
        nodes = construct_nodes(
            construct_releases(
                [entry(x[0](*x[1:], app=app)) for x in entries],
                app=app,
            )[0]
        )
        # Shorthand for "I'll do my own asserts"
        if expected is None:
            return nodes
        if type_ == "release":
            header = nodes[0][0][0].astext()
            assert expected in header
        elif type_ == "issue":
            link = nodes[0][1][0][0][2]
            assert link["refuri"] == expected
        else:
            raise Exception("Gave unknown type_ kwarg to _test_link()!")

    def issues_with_numbers_appear_as_number_links(self):
        self._test_link({}, "issue", "bar_15")

    def releases_appear_as_header_links(self):
        self._test_link({}, "release", "foo_1.0.2")

    def links_will_use_github_option_if_defined(self):
        kwargs = {
            "release_uri": None,
            "issue_uri": None,
            "github_path": "foo/bar",
        }
        for type_, expected in (
            ("issue", "https://github.com/foo/bar/issues/15"),
            ("release", "https://github.com/foo/bar/tree/1.0.2"),
        ):
            self._test_link(kwargs, type_, expected)

    def issue_links_prefer_explicit_setting_over_github_setting(self):
        kwargs = {
            "release_uri": None,
            "issue_uri": "explicit_issue_%s",
            "github_path": "foo/bar",
        }
        self._test_link(kwargs, "issue", "explicit_issue_15")

    def release_links_prefer_explicit_setting_over_github_setting(self):
        kwargs = {
            "release_uri": "explicit_release_%s",
            "issue_uri": None,
            "github_path": "foo/bar",
        }
        self._test_link(kwargs, "release", "explicit_release_1.0.2")

    def links_allow_curlybrace_number_formatting_too(self):
        kwargs = {
            "release_uri": "release_{number}",
            "issue_uri": "issue_{number}",
            "github_path": None,
        }
        self._test_link(kwargs, "release", "release_1.0.2")
        self._test_link(kwargs, "issue", "issue_15")

    def completely_blank_uri_settings_does_not_asplode(self):
        kwargs = {"release_uri": None, "issue_uri": None, "github_path": None}
        # Get nodes for direct inspection
        nodes = self._test_link(kwargs, "release", None)
        # Ensure release entry still displays release version.
        # (These are curently constructed as raw text nodes so no other great
        # way to test this. Meh.)
        text = nodes[0][0][0].astext()
        assert ">1.0.2 <span" in text
        # Ensure issues still display issue number. (Ditto.)
        text = nodes[0][1][0][0].astext()
        assert ">Bug</span>] #15:" in text

    def _assert_prefix(self, entries, expectation):
        assert expectation in self._generate(*entries)[0][0][0]

    def bugs_marked_as_bugs(self):
        self._assert_prefix(["1.0.2", self.b], "Bug")

    def features_marked_as_features(self):
        self._assert_prefix(["1.1.0", self.f], "Feature")

    def support_marked_as_support(self):
        self._assert_prefix(["1.1.0", self.s], "Support")

    def dashed_issues_appear_as_unlinked_issues(self):
        node = self._generate("1.0.2", b("-"))
        assert not isinstance(node[0][2], reference)

    def zeroed_issues_appear_as_unlinked_issues(self):
        node = self._generate("1.0.2", b(0))
        assert not isinstance(node[0][2], reference)

    def un_prefixed_list_items_appear_as_unlinked_bugs(self):
        fake = list_item(
            "",
            paragraph(
                "", "", Text("fixes an issue in "), literal("", "methodname")
            ),
        )
        node = self._generate("1.0.2", fake)
        # [<raw prefix>, <inline colon>, <inline space>, <text>, <monospace>]
        assert len(node[0]) == 5
        assert "Bug" in str(node[0][0])
        assert "fixes an issue" in str(node[0][3])
        assert "methodname" in str(node[0][4])

    def un_prefixed_list_items_get_no_prefix_under_unstable_prehistory(self):
        app = make_app(unstable_prehistory=True)
        fake = list_item("", paragraph("", "", raw("", "whatever")))
        node = self._generate("0.1.0", fake, app=app, skip_initial=True)
        # [<raw bug text>]
        assert len(node[0]) == 1
        assert "Bug" not in str(node[0][0])
        assert "whatever" in str(node[0][0])

    def issues_remain_wrapped_in_unordered_list_nodes(self):
        node = self._generate("1.0.2", self.b, raw=True)[0][1]
        _expect_type(node, bullet_list)
        _expect_type(node[0], list_item)

    def release_headers_have_local_style_tweaks(self):
        node = self._generate("1.0.2", self.b, raw=True)[0][0]
        _expect_type(node, raw)
        # Header w/ bottom margin
        assert '<h2 style="margin-bottom' in str(node)
        # Date span w/ font-size
        assert '<span style="font-size' in str(node)

    def descriptions_are_preserved(self):
        # Changelog containing an issue item w/ trailing node
        issue = list_item(
            "", paragraph("", "", self.b.deepcopy(), raw("", "x"))
        )
        # Trailing nodes should appear post-processing after the link/etc
        rest = self._generate("1.0.2", issue)[0]
        assert len(rest) == 5
        _expect_type(rest[4], raw)
        assert rest[4].astext() == "x"

    def complex_descriptions_are_preserved(self):
        # Complex 'entry' mapping to an outer list_item (list) containing two
        # paragraphs, one w/ the real issue + desc, another simply a 2nd text
        # paragraph. Using 'raw' nodes for filler as needed.
        issue = list_item(
            "",
            paragraph("", "", self.b.deepcopy(), raw("", "x")),
            paragraph("", "", raw("", "y")),
        )
        li = self._generate("1.0.2", issue)
        # Expect that the machinery parsing issue nodes/nodelists, is not
        # discarding our 2nd 'paragraph'
        assert len(li) == 2
        p1, p2 = li
        # Last item in 1st para is our 1st raw node
        _expect_type(p1[4], raw)
        assert p1[4].astext() == "x"
        # Only item in 2nd para is our 2nd raw node
        _expect_type(p2[0], raw)
        assert p2[0].astext() == "y"

    def descriptions_are_parsed_for_issue_roles(self):
        item = list_item("", paragraph("", "", self.b.deepcopy(), s(5)))
        para = self._generate("1.0.2", item)[0]
        # Sanity check - in a broken parsing scenarion, the 4th child will be a
        # raw issue object
        assert not isinstance(para[4], Issue)
        # First/primary link
        _expect_type(para[2], reference)
        assert para[2].astext() == "#15"
        assert "Bug" in para[0].astext()
        # Second/inline link
        _expect_type(para[6], reference)
        assert para[6].astext() == "#5"
        assert "Support" in para[4].astext()

    class unreleased:
        def buckets_omit_major_version_when_only_one_exists(self):
            result = self._generate(b(1), raw=True)[0][0][0]
            html = str(result)  # since repr() from test-fail hides actual text
            assert "Next bugfix release" in html

        def buckets_display_major_version_when_multiple(self):
            entries = (
                b(3),  # should appear in unreleased bugs for 2.x
                "2.0.0",
                b(2),  # should appear in unreleased bugs for 1.x
                "1.0.1",
                b(1),
            )
            # Expectation: [2.x unreleased, 1.x unreleased, 1.0.1]
            two_x, one_x, _ = self._generate(*entries, raw=True)
            html = str(two_x[0][0])
            assert "Next 2.x bugfix release" in html
            html = str(one_x[0][0])
            assert "Next 1.x bugfix release" in html

        def displays_version_when_only_some_lines_displayed(self):
            # I.e. if there's unreleased 1.x stuff but no unreleased 2.x, still
            # display the "1.x".
            entries = (
                # Note lack of any bugfixes post 2.0.0
                "2.0.0",
                b(2),
                "1.0.1",
                b(1),
            )
            # Expectation: [1.x unreleased, 1.0.1] - no 2.x.
            result = self._generate(*entries, raw=True)
            assert len(result) == 2
            html = str(result[0][0][0])
            assert "Next 1.x bugfix release" in html

        def feature_section_links_go_to_development_branch(self):
            self._test_link(
                kwargs=dict(release_uri="releasey_%s"),
                type_="release",
                expected="releasey_master",
                entries=[(f, 15), (release, "1.0.0")],
            )

        def feature_section_links_can_configure_branch_name(self):
            self._test_link(
                kwargs=dict(
                    development_branch="main", release_uri="releasey_%s"
                ),
                type_="release",
                expected="releasey_main",
                entries=[(f, 15), (release, "1.0.0")],
            )

    def unstable_prehistory_active_means_only_one_unreleased_release(self):
        app = make_app(unstable_prehistory=True)
        entries = (b(2), f(3), "0.1.0", b(1))
        result = self._generate(*entries, app=app, raw=True, skip_initial=True)
        html = str(result[0][0][0])
        assert "Next release" in html