source: rtems-central/rtemsqual/content.py @ fa46b668

Last change on this file since fa46b668 was fa46b668, checked in by Sebastian Huber <sebastian.huber@…>, on 05/06/20 at 09:40:24

Move license and copyright registration of items

This avoids a future cyclic dependency between the items and content
modules.

  • Property mode set to 100644
File size: 19.0 KB
Line 
1# SPDX-License-Identifier: BSD-2-Clause
2""" This module provides classes for content generation. """
3
4# Copyright (C) 2019, 2020 embedded brains GmbH (http://www.embedded-brains.de)
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10#    notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright
12#    notice, this list of conditions and the following disclaimer in the
13#    documentation and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
19# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25# POSSIBILITY OF SUCH DAMAGE.
26
27from contextlib import contextmanager
28import itertools
29import os
30import re
31import textwrap
32from typing import Callable, ContextManager, Iterator, List, Optional, Union
33
34from rtemsqual.items import Item
35
36AddContext = Callable[["Content"], ContextManager[None]]
37GenericContent = Union[str, List[str], "Content"]
38
39
40class Copyright:
41    """
42    This class represents a copyright holder with its years of substantial
43    contributions.
44    """
45    def __init__(self, holder):
46        self._holder = holder
47        self._years = set()
48
49    def add_year(self, year: str):
50        """
51        Adds a year to the set of substantial contributions of this copyright
52        holder.
53        """
54        self._years.add(year)
55
56    def get_statement(self) -> str:
57        """ Returns a copyright statement. """
58        line = "Copyright (C)"
59        years = sorted(self._years)
60        year_count = len(years)
61        if year_count == 1:
62            line += " " + years[0]
63        elif year_count > 1:
64            line += " " + years[0] + ", " + years[-1]
65        line += " " + self._holder
66        return line
67
68    def __lt__(self, other: "Copyright") -> bool:
69        # pylint: disable=protected-access
70        if self._years and other._years:
71            self_first_year = sorted(self._years)[0]
72            other_first_year = sorted(other._years)[0]
73            if self_first_year == other_first_year:
74                return self._holder > other._holder
75            return self_first_year > other_first_year
76        if self._years or other._years:
77            return True
78        return self._holder > other._holder
79
80
81class Copyrights:
82    """ This class represents a set of copyright holders. """
83    def __init__(self):
84        self.copyrights = {}
85
86    def register(self, statement):
87        """ Registers a copyright statement. """
88        match = re.search(
89            r"^\s*Copyright\s+\(C\)\s+([0-9]+),\s*([0-9]+)\s+(.+)\s*$",
90            statement,
91            flags=re.I,
92        )
93        if match:
94            holder = match.group(3)
95            the_copyright = self.copyrights.setdefault(holder,
96                                                       Copyright(holder))
97            the_copyright.add_year(match.group(1))
98            the_copyright.add_year(match.group(2))
99            return
100        match = re.search(
101            r"^\s*Copyright\s+\(C\)\s+([0-9]+)\s+(.+)\s*$",
102            statement,
103            flags=re.I,
104        )
105        if match:
106            holder = match.group(2)
107            the_copyright = self.copyrights.setdefault(holder,
108                                                       Copyright(holder))
109            the_copyright.add_year(match.group(1))
110            return
111        match = re.search(r"^\s*Copyright\s+\(C\)\s+(.+)\s*$",
112                          statement,
113                          flags=re.I)
114        if match:
115            holder = match.group(1)
116            self.copyrights.setdefault(holder, Copyright(holder))
117            return
118        raise ValueError(statement)
119
120    def get_statements(self):
121        """ Returns all registered copyright statements as a sorted list. """
122        statements = []
123        for the_copyright in sorted(self.copyrights.values()):
124            statements.append(the_copyright.get_statement())
125        return statements
126
127
128def _make_lines(content: GenericContent) -> List[str]:
129    if isinstance(content, str):
130        return content.strip("\n").split("\n")
131    if isinstance(content, list):
132        return content
133    return content.lines
134
135
136def _indent(lines: List[str], indent: str,
137            empty_line_indent: str) -> List[str]:
138    if indent:
139        return [
140            indent + line if line else empty_line_indent + line
141            for line in lines
142        ]
143    return lines
144
145
146@contextmanager
147def _add_context(_content: "Content") -> Iterator[None]:
148    yield
149
150
151class Content:
152    """ This class builds content. """
153
154    # pylint: disable=too-many-instance-attributes
155    def __init__(self, the_license):
156        self._lines = []  # type: List[str]
157        self._license = the_license
158        self._copyrights = Copyrights()
159        self._gap = False
160        self._tab = "  "
161        self._indents = [""]
162        self._indent = ""
163        self._empty_line_indents = [""]
164        self._empty_line_indent = ""
165
166    def __str__(self):
167        return "\n".join(itertools.chain(self._lines, [""]))
168
169    @property
170    def lines(self) -> List[str]:
171        """ The lines. """
172        return self._lines
173
174    def append(self, content: GenericContent) -> None:
175        """ Appends the content. """
176        self._lines.extend(
177            _indent(_make_lines(content), self._indent,
178                    self._empty_line_indent))
179
180    def prepend(self, content: GenericContent) -> None:
181        """ Prepends the content. """
182        self._lines[0:0] = _indent(_make_lines(content), self._indent,
183                                   self._empty_line_indent)
184
185    def add(self,
186            content: Optional[GenericContent],
187            context: AddContext = _add_context) -> None:
188        """
189        Skips leading empty lines, adds a gap if needed, then adds the content.
190        """
191        if not content:
192            return
193        lines = _make_lines(content)
194        index = 0
195        for line in lines:
196            if line:
197                if self._gap:
198                    self._lines.extend(
199                        _indent([""], self._indent, self._empty_line_indent))
200                self._gap = True
201                with context(self):
202                    self._lines.extend(
203                        _indent(lines[index:], self._indent,
204                                self._empty_line_indent))
205                break
206            index += 1
207
208    @property
209    def gap(self) -> bool:
210        """
211        True if the next Content.add() adds a gap before the new content,
212        otherwise False.
213        """
214        return self._gap
215
216    @gap.setter
217    def gap(self, value: bool) -> None:
218        """ Sets the gap indicator for Content.add(). """
219        self._gap = value
220
221    def _update_indent(self) -> None:
222        self._indent = "".join(self._indents)
223        empty_line_indent = "".join(self._empty_line_indents)
224        if empty_line_indent.isspace():
225            self._empty_line_indent = ""
226        else:
227            self._empty_line_indent = empty_line_indent
228
229    def push_indent(self,
230                    indent: Optional[str] = None,
231                    empty_line_indent: Optional[str] = None) -> None:
232        """ Pushes an indent level. """
233        self._indents.append(indent if indent else self._tab)
234        self._empty_line_indents.append(
235            empty_line_indent if empty_line_indent else self._tab)
236        self._update_indent()
237
238    def pop_indent(self) -> None:
239        """ Pops an indent level. """
240        self._indents.pop()
241        self._empty_line_indents.pop()
242        self._update_indent()
243
244    @contextmanager
245    def indent(self,
246               indent: Optional[str] = None,
247               empty_line_indent: Optional[str] = None) -> Iterator[None]:
248        """ Opens an indent context. """
249        self.push_indent(indent, empty_line_indent)
250        yield
251        self.pop_indent()
252
253    def indent_lines(self, level: int) -> None:
254        """ Indents all lines by the specified indent level. """
255        prefix = level * self._tab
256        self._lines = [prefix + line if line else line for line in self._lines]
257
258    def add_blank_line(self):
259        """ Adds a blank line. """
260        self._lines.append("")
261
262    def register_license(self, the_license: str) -> None:
263        """ Registers a licence for the content. """
264        licenses = re.split(r"\s+OR\s+", the_license)
265        if self._license not in licenses:
266            raise ValueError(the_license)
267
268    def register_copyright(self, statement: str) -> None:
269        """ Registers a copyright statement for the content. """
270        self._copyrights.register(statement)
271
272    def register_license_and_copyrights_of_item(self, item: Item) -> None:
273        """ Registers the license and copyrights of the item. """
274        self.register_license(item["SPDX-License-Identifier"])
275        for statement in item["copyrights"]:
276            self.register_copyright(statement)
277
278    def write(self, path: str) -> None:
279        """ Writes the content to the file specified by the path. """
280        directory = os.path.dirname(path)
281        if directory:
282            os.makedirs(directory, exist_ok=True)
283        with open(path, "w+") as out:
284            out.write(str(self))
285
286
287class SphinxContent(Content):
288    """ This class builds Sphinx content. """
289    def __init__(self):
290        super().__init__("CC-BY-SA-4.0")
291        self._tab = "    "
292
293    def add_label(self, label: str) -> None:
294        """ Adds a label. """
295        self.add(".. _" + label.strip() + ":")
296
297    def add_header(self, name, level="=") -> None:
298        """ Adds a header. """
299        name = name.strip()
300        self.add([name, level * len(name)])
301
302    def add_index_entries(self, entries) -> None:
303        """ Adds a list of index entries the content. """
304        self.add([".. index:: " + entry for entry in _make_lines(entries)])
305
306    def add_definition_item(self, name, lines) -> None:
307        """ Adds a definition item the content. """
308        @contextmanager
309        def _definition_item_context(content: Content) -> Iterator[None]:
310            content.append(name)
311            content.push_indent()
312            yield
313            content.pop_indent()
314
315        self.add(lines, _definition_item_context)
316
317    def add_licence_and_copyrights(self) -> None:
318        """
319        Adds a licence and copyright block according to the registered licenses
320        and copyrights.
321        """
322        statements = self._copyrights.get_statements()
323        if statements:
324            self.prepend("")
325            self.prepend([f".. {stm}" for stm in statements])
326        self.prepend([f".. SPDX-License-Identifier: {self._license}", ""])
327
328
329class MacroToSphinx:
330    """ This class expands specification item macros to Sphinx markup. """
331    def __init__(self):
332        self._terms = {}
333
334    def set_terms(self, terms):
335        """ Sets the glossary of terms used for macro expansion. """
336        self._terms = terms
337
338    def substitute(self, text):
339        """
340        Substitutes all specification item macros contained in the text.
341        """
342        return re.sub(r"@@|@([a-z]+){([^}]+)}", self, text)
343
344    def __call__(self, match):
345        name = match.group(1)
346        if name:
347            roles = {
348                "term":
349                lambda x: ":term:`" + self._terms[x]["glossary-term"] + "`"
350            }
351            return roles[name](match.group(2))
352        assert match.group(0) == "@@"
353        return "@"
354
355
356_BSD_2_CLAUSE_LICENSE = """Redistribution and use in source and binary \
357forms, with or without
358modification, are permitted provided that the following conditions
359are met:
3601. Redistributions of source code must retain the above copyright
361   notice, this list of conditions and the following disclaimer.
3622. Redistributions in binary form must reproduce the above copyright
363   notice, this list of conditions and the following disclaimer in the
364   documentation and/or other materials provided with the distribution.
365
366THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
367AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
368IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
369ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
370LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
371CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
372SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
373INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
374CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
375ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
376POSSIBILITY OF SUCH DAMAGE."""
377
378
379class CContent(Content):
380    """ This class builds C content. """
381
382    # pylint: disable=too-many-public-methods
383    def __init__(self):
384        super().__init__("BSD-2-Clause")
385
386    def add_spdx_license_identifier(self):
387        """
388        Adds an SPDX License Identifier according to the registered licenses.
389        """
390        self.prepend([f"/* SPDX-License-Identifier: {self._license} */", ""])
391
392    def add_copyrights_and_licenses(self):
393        """
394        Adds the copyrights and licenses according to the registered copyrights
395        and licenses.
396        """
397        with self.comment_block():
398            self.add(self._copyrights.get_statements())
399            self.add(_BSD_2_CLAUSE_LICENSE)
400
401    def add_have_config(self):
402        """ Adds a guarded config.h include. """
403        self.add(["#ifdef HAVE_CONFIG_H", "#include \"config.h\"", "#endif"])
404
405    def add_includes(self, includes: List[str], local: bool = False) -> None:
406        """ Adds a block of includes. """
407        class IncludeKey:  # pylint: disable=too-few-public-methods
408            """ Provides a key to sort includes. """
409            def __init__(self, inc: str):
410                self._inc = inc
411
412            def __lt__(self, other: "IncludeKey") -> bool:
413                left = self._inc.split("/")
414                right = other._inc.split("/")
415                left_len = len(left)
416                right_len = len(right)
417                if left_len == right_len:
418                    for left_part, right_part in zip(left[:-1], right[:-1]):
419                        if left_part != right_part:
420                            return left_part < right_part
421                    return left[-1] < right[-1]
422                return left_len < right_len
423
424        left = "\"" if local else "<"
425        right = "\"" if local else ">"
426        self.add([
427            f"#include {left}{inc}{right}"
428            for inc in sorted(set(includes), key=IncludeKey)
429        ])
430
431    def wrap(self, content: Optional[str], intro: str = "") -> List[str]:
432        """ Wraps a text. """
433        if not content:
434            return [""]
435        wrapper = textwrap.TextWrapper()
436        wrapper.drop_whitespace = True
437        wrapper.initial_indent = intro
438        wrapper.subsequent_indent = len(intro) * " "
439        wrapper.width = 79 - len(self._indent)
440        return wrapper.wrap(content)
441
442    def _open_comment_block(self, begin) -> None:
443        self.add(begin)
444        self.push_indent(" * ", " *")
445        self.gap = False
446
447    def open_comment_block(self) -> None:
448        """ Opens a comment block. """
449        self._open_comment_block("/*")
450
451    def open_doxygen_block(self) -> None:
452        """ Opens a Doxygen comment block. """
453        self._open_comment_block("/**")
454
455    def open_file_block(self) -> None:
456        """ Opens a Doxygen @file comment block. """
457        self._open_comment_block(["/**", " * @file"])
458        self.gap = True
459
460    def open_defgroup_block(self, identifier: str, name: str) -> None:
461        """ Opens a Doxygen @defgroup comment block. """
462        self._open_comment_block(["/**", f" * @defgroup {identifier} {name}"])
463        self.gap = True
464
465    def open_function_block(self, function: str) -> None:
466        """ Opens a Doxygen @fn comment block. """
467        self._open_comment_block(["/**", f" * @fn {function}"])
468        self.gap = True
469
470    def close_comment_block(self) -> None:
471        """ Closes a comment block. """
472        self.pop_indent()
473        self.append(" */")
474        self.gap = True
475
476    @contextmanager
477    def comment_block(self) -> Iterator[None]:
478        """ Opens a comment block context. """
479        self.open_comment_block()
480        yield
481        self.close_comment_block()
482
483    @contextmanager
484    def doxygen_block(self) -> Iterator[None]:
485        """ Opens a Doxygen comment block context. """
486        self.open_doxygen_block()
487        yield
488        self.close_comment_block()
489
490    @contextmanager
491    def file_block(self) -> Iterator[None]:
492        """ Opens a Doxygen @file comment block context. """
493        self.open_file_block()
494        yield
495        self.close_comment_block()
496
497    @contextmanager
498    def defgroup_block(self, identifier: str, name: str) -> Iterator[None]:
499        """ Opens a Doxygen @defgroup comment block context. """
500        self.open_defgroup_block(identifier, name)
501        yield
502        self.close_comment_block()
503
504    @contextmanager
505    def function_block(self, function: str) -> Iterator[None]:
506        """ Opens a Doxygen @fn comment block context. """
507        self.open_function_block(function)
508        yield
509        self.close_comment_block()
510
511    def add_brief_description(self, description: Optional[str]) -> None:
512        """ Adds a brief description. """
513        return self.add(self.wrap(description, intro="@brief "))
514
515    def add_ingroup(self, ingroups: List[str]) -> None:
516        """ Adds an ingroup comment block. """
517        self.add(["@ingroup " + ingroup for ingroup in sorted(set(ingroups))])
518
519    def add_group(self, identifier: str, name: str, ingroups: List[str],
520                  brief: Optional[str], description: Optional[str]) -> None:
521        # pylint: disable=too-many-arguments
522        """ Adds a group definition. """
523        with self.defgroup_block(identifier, name):
524            self.add_ingroup(ingroups)
525            self.add_brief_description(brief)
526            self.add(self.wrap(description))
527
528    @contextmanager
529    def header_guard(self, filename: str) -> Iterator[None]:
530        """ Opens a header guard context. """
531        filename = os.path.basename(filename)
532        guard = "_" + filename.replace(".", "_").upper()
533        self.add([f"#ifndef {guard}", f"#define {guard}"])
534        yield
535        self.add(f"#endif /* {guard} */")
536
537    @contextmanager
538    def extern_c(self) -> Iterator[None]:
539        """ Opens an extern "C" context. """
540        self.add(["#ifdef __cplusplus", "extern \"C\" {", "#endif"])
541        yield
542        self.add(["#ifdef __cplusplus", "}", "#endif"])
Note: See TracBrowser for help on using the repository browser.