source: rtems-central/rtemsqual/items.py @ 48cdcc1

Last change on this file since 48cdcc1 was 48cdcc1, checked in by Sebastian Huber <sebastian.huber@…>, on 04/23/20 at 10:36:46

items: Add Item.load()

  • Property mode set to 100644
File size: 8.9 KB
Line 
1# SPDX-License-Identifier: BSD-2-Clause
2""" This module provides specification items and an item cache. """
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
27import os
28import pickle
29import stat
30from typing import Any, List, Dict
31import yaml
32
33from rtemsqual.content import SphinxContent
34
35ItemList = List["Item"]
36ItemMap = Dict[str, "Item"]
37
38
39def _is_enabled_op_and(enabled: List[str], enabled_by: Any) -> bool:
40    for next_enabled_by in enabled_by:
41        if not _is_enabled(enabled, next_enabled_by):
42            return False
43    return True
44
45
46def _is_enabled_op_false(_enabled: List[str], _enabled_by: Any) -> bool:
47    return False
48
49
50def _is_enabled_op_not(enabled: List[str], enabled_by: Any) -> bool:
51    return not _is_enabled(enabled, enabled_by)
52
53
54def _is_enabled_op_or(enabled: List[str], enabled_by: Any) -> bool:
55    for next_enabled_by in enabled_by:
56        if _is_enabled(enabled, next_enabled_by):
57            return True
58    return False
59
60
61_IS_ENABLED_OP = {
62    "and": _is_enabled_op_and,
63    "not": _is_enabled_op_not,
64    "or": _is_enabled_op_or
65}
66
67
68def _is_enabled(enabled: List[str], enabled_by: Any) -> bool:
69    if enabled_by:
70        if isinstance(enabled_by, list):
71            return _is_enabled_op_or(enabled, enabled_by)
72        if isinstance(enabled_by, dict):
73            if len(enabled_by) == 1:
74                key = next(iter(enabled_by))
75                return _IS_ENABLED_OP.get(key, _is_enabled_op_false)(
76                    enabled, enabled_by[key])
77            return False
78        return enabled_by in enabled
79    return True
80
81
82def _str_representer(dumper, data):
83    return dumper.represent_scalar("tag:yaml.org,2002:str",
84                                   data,
85                                   style="|" if "\n" in data else "")
86
87
88yaml.add_representer(str, _str_representer)
89
90
91class Item:
92    """ Objects of this class represent a specification item. """
93    def __init__(self, uid: str, data: Any):
94        self._uid = uid
95        self._data = data
96        self._links = []  # type: ItemList
97        self._children = []  # type: ItemList
98
99    def __contains__(self, key: str) -> bool:
100        return key in self._data
101
102    def __getitem__(self, name: str) -> Any:
103        return self._data[name]
104
105    @property
106    def uid(self) -> str:
107        """ Returns the UID of the item. """
108        return self._uid
109
110    def to_abs_uid(self, abs_or_rel_uid: str) -> str:
111        """
112        Returns the absolute UID of an absolute UID or an UID relative to this
113        item.
114        """
115        if os.path.isabs(abs_or_rel_uid):
116            return abs_or_rel_uid
117        return os.path.normpath(
118            os.path.join(os.path.dirname(self.uid), abs_or_rel_uid))
119
120    @property
121    def parents(self) -> ItemList:
122        """ Returns the list of parents of this items. """
123        return self._links
124
125    @property
126    def children(self) -> ItemList:
127        """ Returns the list of children of this items. """
128        return self._children
129
130    def init_parents(self, item_cache: "ItemCache"):
131        """ Initializes the list of parents of this items. """
132        for link in self._data["links"]:
133            self._links.append(item_cache[self.to_abs_uid(link["uid"])])
134
135    def add_child(self, child: "Item"):
136        """ Adds a child to this item. """
137        self._children.append(child)
138
139    def register_license_and_copyrights(self, content: SphinxContent):
140        """ Registers the license and copyrights of this item. """
141        content.register_license(self["SPDX-License-Identifier"])
142        for statement in self["copyrights"]:
143            content.register_copyright(statement)
144
145    def is_enabled(self, enabled: List[str]):
146        """ Returns true if the item is enabled by the specified enables. """
147        return _is_enabled(enabled, self["enabled-by"])
148
149    @property
150    def file(self) -> str:
151        """ Returns the file of the item. """
152        return self._data["_file"]
153
154    @file.setter
155    def file(self, value: str):
156        """ Sets the file of the item. """
157        self._data["_file"] = value
158
159    def save(self):
160        """ Saves the item to the corresponding file. """
161        with open(self.file, "w") as dst:
162            data = self._data.copy()
163            del data["_file"]
164            dst.write(
165                yaml.dump(data, default_flow_style=False, allow_unicode=True))
166
167    def load(self):
168        """ Loads the item from the corresponding file. """
169        filename = self.file
170        with open(filename, "r") as src:
171            self._data = yaml.safe_load(src.read())
172            self._data["_file"] = filename
173
174
175class ItemCache:
176    """ This class provides a cache of specification items. """
177    def __init__(self, config: Any):
178        self._items = {}  # type: ItemMap
179        self._top_level = {}  # type: ItemMap
180        self._load_items(config)
181
182    def __getitem__(self, uid: str) -> Item:
183        return self._items[uid]
184
185    @property
186    def all(self) -> ItemMap:
187        """ Returns the map of all specification items. """
188        return self._items
189
190    @property
191    def top_level(self) -> ItemMap:
192        """ Returns the map of top-level specification items. """
193        return self._top_level
194
195    def _load_items_in_dir(self, base: str, path: str, cache_file: str,
196                           update_cache: bool) -> None:
197        data_by_uid = {}  # type: Dict[str, Any]
198        if update_cache:
199            for name in os.listdir(path):
200                path2 = os.path.join(path, name)
201                if name.endswith(".yml") and not name.startswith("."):
202                    uid = "/" + os.path.relpath(path2, base).replace(
203                        ".yml", "")
204                    with open(path2, "r") as yaml_src:
205                        data = yaml.safe_load(yaml_src.read())
206                        data["_file"] = os.path.abspath(path2)
207                        data_by_uid[uid] = data
208            os.makedirs(os.path.dirname(cache_file), exist_ok=True)
209            with open(cache_file, "wb") as out:
210                pickle.dump(data_by_uid, out)
211        else:
212            with open(cache_file, "rb") as pickle_src:
213                data_by_uid = pickle.load(pickle_src)
214        for uid, data in data_by_uid.items():
215            item = Item(uid, data)
216            self._items[uid] = item
217            if not item["links"]:
218                self._top_level[uid] = item
219
220    def _load_items_recursive(self, base: str, path: str,
221                              cache_dir: str) -> None:
222        mid = os.path.abspath(path)
223        mid = mid.replace(os.path.commonprefix([cache_dir, mid]), "")
224        cache_file = os.path.join(cache_dir, mid, "spec.pickle")
225        try:
226            mtime = os.path.getmtime(cache_file)
227            update_cache = False
228        except FileNotFoundError:
229            update_cache = True
230        for name in os.listdir(path):
231            path2 = os.path.join(path, name)
232            if name.endswith(".yml") and not name.startswith("."):
233                update_cache = update_cache or mtime <= os.path.getmtime(path2)
234            else:
235                if stat.S_ISDIR(os.lstat(path2).st_mode):
236                    self._load_items_recursive(base, path2, cache_dir)
237        self._load_items_in_dir(base, path, cache_file, update_cache)
238
239    def _init_parents(self) -> None:
240        for item in self._items.values():
241            item.init_parents(self)
242
243    def _init_children(self) -> None:
244        for item in self._items.values():
245            for parent in item.parents:
246                parent.add_child(item)
247
248    def _load_items(self, config: Any) -> None:
249        cache_dir = os.path.abspath(config["cache-directory"])
250        for path in config["paths"]:
251            self._load_items_recursive(path, path, cache_dir)
252        self._init_parents()
253        self._init_children()
Note: See TracBrowser for help on using the repository browser.