source: rtems-central/rtemsqual/content.py @ 05246b3

Last change on this file since 05246b3 was 99c6449, checked in by Sebastian Huber <sebastian.huber@…>, on 05/09/20 at 15:45:46

content: Change gap after indent

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