| Total Complexity | 261 |
| Total Lines | 1582 |
| Duplicated Lines | 2.72 % |
| Changes | 0 | ||
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like doorstop.core.item often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | """Representation of an item in a document.""" |
||
|
|
|||
| 2 | |||
| 3 | import os |
||
| 4 | import re |
||
| 5 | import functools |
||
| 6 | |||
| 7 | import pyficache |
||
| 8 | |||
| 9 | from doorstop import common |
||
| 10 | from doorstop.common import DoorstopError, DoorstopWarning, DoorstopInfo |
||
| 11 | from doorstop.core.base import (add_item, edit_item, delete_item, |
||
| 12 | auto_load, auto_save, |
||
| 13 | BaseValidatable, BaseFileObject) |
||
| 14 | from doorstop.core.types import Prefix, UID, Text, Level, Stamp, to_bool, REF #added REF |
||
| 15 | from doorstop.core import editor |
||
| 16 | from doorstop import settings |
||
| 17 | |||
| 18 | log = common.logger(__name__) |
||
| 19 | |||
| 20 | |||
| 21 | def requires_tree(func): |
||
| 22 | """Decorator for methods that require a tree reference.""" |
||
| 23 | @functools.wraps(func) |
||
| 24 | def wrapped(self, *args, **kwargs): |
||
| 25 | """Wrapped method that requires a tree reference.""" |
||
| 26 | if not self.tree: |
||
| 27 | name = func.__name__ |
||
| 28 | log.critical("`{}` can only be called with a tree".format(name)) |
||
| 29 | return None |
||
| 30 | return func(self, *args, **kwargs) |
||
| 31 | return wrapped |
||
| 32 | |||
| 33 | |||
| 34 | def requires_document(func): |
||
| 35 | """Decorator for methods that require a document reference.""" |
||
| 36 | @functools.wraps(func) |
||
| 37 | def wrapped(self, *args, **kwargs): |
||
| 38 | """Wrapped method that requires a document reference.""" |
||
| 39 | if not self.document: |
||
| 40 | name = func.__name__ |
||
| 41 | msg = "`{}` can only be called with a document".format(name) |
||
| 42 | log.critical(msg) |
||
| 43 | return None |
||
| 44 | return func(self, *args, **kwargs) |
||
| 45 | return wrapped |
||
| 46 | |||
| 47 | |||
| 48 | class Item(BaseValidatable, BaseFileObject): # pylint: disable=R0902 |
||
| 49 | |||
| 50 | """Represents an item file with linkable text.""" |
||
| 51 | |||
| 52 | EXTENSIONS = '.yml', '.yaml' |
||
| 53 | |||
| 54 | DEFAULT_LEVEL = Level('1.0') |
||
| 55 | DEFAULT_ACTIVE = True |
||
| 56 | DEFAULT_NORMATIVE = True |
||
| 57 | DEFAULT_DERIVED = False |
||
| 58 | DEFAULT_REVIEWED = Stamp() |
||
| 59 | DEFAULT_TEXT = Text() |
||
| 60 | DEFAULT_REF = "" |
||
| 61 | # add 02.12.2019 |
||
| 62 | DEFAULT_Is_Req = True |
||
| 63 | |||
| 64 | DEFAULT_SPEC_RATIONALE = Text() |
||
| 65 | DEFAULT_SPEC_STATUS = "In_Analysis" |
||
| 66 | DEFAULT_SPEC_SHORT_DECRIPTION = "" |
||
| 67 | DEFAULT_SPEC_ID = "" |
||
| 68 | DEFAULT_SPEC_VERSION = 1 |
||
| 69 | DEFAULT_Assumption = "" |
||
| 70 | DEFAULT_Ad_Info = "" |
||
| 71 | DEFAULT_Author = "" |
||
| 72 | DEFAULT_Generation = "Manual" |
||
| 73 | DEFAULT_REFINE = "" |
||
| 74 | DEFAULT_ASSOCIATED = "" |
||
| 75 | DEFAULT_Validation_Ref = "" |
||
| 76 | DEFAULT_Validation_Mean = "Review" |
||
| 77 | DEFAULT_Verification_Ref = "" |
||
| 78 | DEFAULT_Verification_Mean = "LAB_TEST" |
||
| 79 | DEFAULT_Verification_Rationale = "" |
||
| 80 | DEFAULT_Verification_Status = "Analysis" |
||
| 81 | DEFAULT_VoV_Ref = "" |
||
| 82 | DEFAULT_Alllocation = "" |
||
| 83 | |||
| 84 | # end add |
||
| 85 | |||
| 86 | # add 15.01.2020 |
||
| 87 | DEFAULT_SIDEBAR='' |
||
| 88 | DEFAULT_EXTENSION='' |
||
| 89 | DEFAULT_TITLE='' |
||
| 90 | # end add |
||
| 91 | |||
| 92 | |||
| 93 | |||
| 94 | |||
| 95 | def __init__(self, path, root=os.getcwd(), **kwargs): |
||
| 96 | """Initialize an item from an existing file. |
||
| 97 | |||
| 98 | :param path: path to Item file |
||
| 99 | :param root: path to root of project |
||
| 100 | |||
| 101 | """ |
||
| 102 | super().__init__() |
||
| 103 | # Ensure the path is valid |
||
| 104 | if not os.path.isfile(path): |
||
| 105 | raise DoorstopError("item does not exist: {}".format(path)) |
||
| 106 | # Ensure the filename is valid |
||
| 107 | filename = os.path.basename(path) |
||
| 108 | name, ext = os.path.splitext(filename) |
||
| 109 | try: |
||
| 110 | UID(name).check() |
||
| 111 | except DoorstopError: |
||
| 112 | msg = "invalid item filename: {}".format(filename) |
||
| 113 | raise DoorstopError(msg) from None |
||
| 114 | # Ensure the file extension is valid |
||
| 115 | if ext.lower() not in self.EXTENSIONS: |
||
| 116 | msg = "'{0}' extension not in {1}".format(path, self.EXTENSIONS) |
||
| 117 | raise DoorstopError(msg) |
||
| 118 | # Initialize the item |
||
| 119 | self.path = path |
||
| 120 | self.root = root |
||
| 121 | self.document = kwargs.get('document') |
||
| 122 | self.tree = kwargs.get('tree') |
||
| 123 | self.auto = kwargs.get('auto', Item.auto) |
||
| 124 | # Set default values |
||
| 125 | self._data['level'] = Item.DEFAULT_LEVEL |
||
| 126 | self._data['active'] = Item.DEFAULT_ACTIVE |
||
| 127 | self._data['normative'] = Item.DEFAULT_NORMATIVE |
||
| 128 | self._data['derived'] = Item.DEFAULT_DERIVED |
||
| 129 | self._data['reviewed'] = Item.DEFAULT_REVIEWED |
||
| 130 | self._data['text'] = Item.DEFAULT_TEXT |
||
| 131 | self._data['ref'] = set() |
||
| 132 | self._data['links'] = set() |
||
| 133 | |||
| 134 | |||
| 135 | |||
| 136 | # add 02.12.2019 / 06.12.2019 |
||
| 137 | self._data['Is_Req'] = Item.DEFAULT_Is_Req |
||
| 138 | |||
| 139 | self._data['SPEC_RATIONALE'] = Item.DEFAULT_SPEC_RATIONALE |
||
| 140 | self._data['SPEC_SHORT_DECRIPTION'] = Item.DEFAULT_SPEC_SHORT_DECRIPTION |
||
| 141 | self._data['SPEC_ID'] = Item.DEFAULT_SPEC_ID |
||
| 142 | self._data['SPEC_VERSION'] = Item.DEFAULT_SPEC_VERSION |
||
| 143 | #self._data['SPEC_TEXT'] = Item.DEFAULT_SPEC_TEXT |
||
| 144 | self._data['Assumption'] = Item.DEFAULT_Assumption |
||
| 145 | self._data['Add_Info'] = Item.DEFAULT_Ad_Info |
||
| 146 | self._data['Author'] = Item.DEFAULT_Author |
||
| 147 | self._data['Generation'] = Item.DEFAULT_Generation |
||
| 148 | self._data['REFINE'] = Item.DEFAULT_REFINE |
||
| 149 | self._data['ASSOCIATED'] = Item.DEFAULT_ASSOCIATED |
||
| 150 | self._data['SPEC_STATUS'] = Item.DEFAULT_SPEC_STATUS |
||
| 151 | self._data['Validation_Ref'] = Item.DEFAULT_Validation_Ref |
||
| 152 | self._data['Validation_Mean'] = Item.DEFAULT_Validation_Mean |
||
| 153 | self._data['Verification_Ref'] = Item.DEFAULT_Verification_Ref |
||
| 154 | self._data['Verification_Mean'] = Item.DEFAULT_Verification_Mean |
||
| 155 | self._data['Verification_Rationale'] = Item.DEFAULT_Verification_Rationale |
||
| 156 | self._data['Verification_Status'] = Item.DEFAULT_Verification_Status |
||
| 157 | self._data['VoV_Ref'] = Item.DEFAULT_VoV_Ref |
||
| 158 | self._data['Allocation'] = Item.DEFAULT_Alllocation |
||
| 159 | |||
| 160 | # add 15.01.2020 |
||
| 161 | # Hinzufügen von attributen die nur auftreten wenn es sich bei dem yaml file nicht um ein requirmenet sondern |
||
| 162 | # um eine Beschreibung überschrift oder sonstiges handelt... |
||
| 163 | self._NO_REQ_data['EXTENSION']= Item.DEFAULT_EXTENSION |
||
| 164 | self._NO_REQ_data['TITLE']=Item.DEFAULT_TITLE |
||
| 165 | self._NO_REQ_data['SIDEBAR']= Item.DEFAULT_SIDEBAR |
||
| 166 | # end add |
||
| 167 | |||
| 168 | |||
| 169 | # end add |
||
| 170 | |||
| 171 | def __repr__(self): |
||
| 172 | return "Item('{}')".format(self.path) |
||
| 173 | |||
| 174 | def __str__(self): |
||
| 175 | if common.verbosity < common.STR_VERBOSITY: |
||
| 176 | return str(self.uid) |
||
| 177 | else: |
||
| 178 | return "{} ({})".format(self.uid, self.relpath) |
||
| 179 | |||
| 180 | def __lt__(self, other): |
||
| 181 | if self.level == other.level: |
||
| 182 | return self.uid < other.uid |
||
| 183 | else: |
||
| 184 | return self.level < other.level |
||
| 185 | |||
| 186 | @staticmethod |
||
| 187 | @add_item |
||
| 188 | def new(tree, document, path, root, uid, level=None, auto=None): # pylint: disable=R0913 |
||
| 189 | """Internal method to create a new item. |
||
| 190 | |||
| 191 | :param tree: reference to the tree that contains this item |
||
| 192 | :param document: reference to document that contains this item |
||
| 193 | |||
| 194 | :param path: path to directory for the new item |
||
| 195 | :param root: path to root of the project |
||
| 196 | :param uid: UID for the new item |
||
| 197 | |||
| 198 | :param level: level for the new item |
||
| 199 | :param auto: automatically save the item |
||
| 200 | |||
| 201 | :raises: :class:`~doorstop.common.DoorstopError` if the item |
||
| 202 | already exists |
||
| 203 | |||
| 204 | :return: new :class:`~doorstop.core.item.Item` |
||
| 205 | |||
| 206 | """ |
||
| 207 | UID(uid).check() |
||
| 208 | filename = str(uid) + Item.EXTENSIONS[0] |
||
| 209 | path2 = os.path.join(path, filename) |
||
| 210 | # Create the initial item file |
||
| 211 | log.debug("creating item file at {}...".format(path2)) |
||
| 212 | Item._create(path2, name='item') |
||
| 213 | # Initialize the item |
||
| 214 | item = Item(path2, root=root, document=document, tree=tree, auto=False) |
||
| 215 | item.level = level if level is not None else item.level |
||
| 216 | if auto or (auto is None and Item.auto): |
||
| 217 | item.save() |
||
| 218 | # Return the item |
||
| 219 | return item |
||
| 220 | |||
| 221 | def load(self, reload=False): |
||
| 222 | """Load the item's properties from its file.""" |
||
| 223 | if self._loaded and not reload: |
||
| 224 | return |
||
| 225 | log.debug("loading {}...".format(repr(self))) |
||
| 226 | # Read text from file |
||
| 227 | text = self._read(self.path) |
||
| 228 | # Parse YAML data from text |
||
| 229 | data = self._load(text, self.path) |
||
| 230 | |||
| 231 | # Store parsed data |
||
| 232 | # Herrausfinden on das YMAL file ein Requirement ist oder nicht... |
||
| 233 | # Danach darus schlussfolgern welche attribute in dem file auftauche sollen und welche nicht 15.01.2020 |
||
| 234 | # Als erstes wird geschaut ob das data dictionary überhaupt daten entält .... |
||
| 235 | if bool(data): |
||
| 236 | if data['Is_Req']: |
||
| 237 | |||
| 238 | # tmp list to check for values that are only displayed when item is not a requirement ... |
||
| 239 | tmp_list= ['EXTENSION','TITLE','SIDEBAR'] |
||
| 240 | |||
| 241 | |||
| 242 | for key, value in data.items(): |
||
| 243 | |||
| 244 | #Is_Req add => vllt später anders um andere attribute zu sperren 09.01.2020 |
||
| 245 | if key == 'Is_Req': |
||
| 246 | values = to_bool(value) |
||
| 247 | #end Is_Req add |
||
| 248 | if key == 'level': |
||
| 249 | value = Level(value) |
||
| 250 | elif key == 'active': |
||
| 251 | value = to_bool(value) |
||
| 252 | elif key == 'normative': |
||
| 253 | value = to_bool(value) |
||
| 254 | elif key == 'derived': |
||
| 255 | value = to_bool(value) |
||
| 256 | elif key == 'reviewed': |
||
| 257 | value = Stamp(value) |
||
| 258 | elif key == 'text': |
||
| 259 | #changed behhaviour of Text class in types.py to let linebreaks in ... |
||
| 260 | value = Text(value) |
||
| 261 | # add 02.12.2019 |
||
| 262 | elif key == 'SPEC_RATIONALE': |
||
| 263 | value = Text(value) |
||
| 264 | elif key == 'SPEC_STATUS': |
||
| 265 | value = Text(value) |
||
| 266 | # end add |
||
| 267 | |||
| 268 | # add 09.12.2019 |
||
| 269 | elif key == 'ref': |
||
| 270 | value = set(REF(part) for part in value) |
||
| 271 | # print(value) |
||
| 272 | # end add |
||
| 273 | elif key == 'links': |
||
| 274 | # print(type(value)) |
||
| 275 | value = set(UID(part) for part in value) |
||
| 276 | # print(type(value)) |
||
| 277 | # print(value) |
||
| 278 | elif key in tmp_list: |
||
| 279 | continue |
||
| 280 | |||
| 281 | else: |
||
| 282 | if isinstance(value, str): |
||
| 283 | value = Text(value) |
||
| 284 | |||
| 285 | self._data[key] = value |
||
| 286 | # add 15.01.2020 |
||
| 287 | else: |
||
| 288 | |||
| 289 | for key, value in data.items(): |
||
| 290 | |||
| 291 | if key == 'Is_Req': |
||
| 292 | values = to_bool(value) |
||
| 293 | self._data[key] = value |
||
| 294 | |||
| 295 | elif key == 'level': |
||
| 296 | value = Level(value) |
||
| 297 | self._data[key] = value |
||
| 298 | |||
| 299 | elif key == 'text': |
||
| 300 | value = Text(value) |
||
| 301 | self._data[key] = value |
||
| 302 | |||
| 303 | |||
| 304 | elif key == 'EXTENSION': |
||
| 305 | value=Text(value.upper()) |
||
| 306 | self._NO_REQ_data[key] = value |
||
| 307 | elif key == 'TITLE': |
||
| 308 | value = Text(value) |
||
| 309 | self._NO_REQ_data[key] = value |
||
| 310 | elif key == 'SIDEBAR': |
||
| 311 | value =Text(value) |
||
| 312 | self._NO_REQ_data[key] = value |
||
| 313 | else: |
||
| 314 | # format not know |
||
| 315 | # will not be therer anymore after this procedure |
||
| 316 | continue |
||
| 317 | |||
| 318 | # Set meta attributes |
||
| 319 | self._loaded = True# |
||
| 320 | |||
| 321 | @edit_item |
||
| 322 | def save(self): |
||
| 323 | """Format and save the item's properties to its file.""" |
||
| 324 | log.debug("saving {}...".format(repr(self))) |
||
| 325 | # Format the data items |
||
| 326 | data = self.data |
||
| 327 | # Dump the data to YAML |
||
| 328 | text = self._dump(data) |
||
| 329 | # Save the YAML to file |
||
| 330 | self._write(text, self.path) |
||
| 331 | # Set meta attributes |
||
| 332 | self._loaded = False |
||
| 333 | self.auto = True |
||
| 334 | |||
| 335 | # properties ############################################################# |
||
| 336 | |||
| 337 | # add 09.12.2020 change behaviour for yaml dumping if file is not a requirement |
||
| 338 | @property |
||
| 339 | @auto_load |
||
| 340 | def data(self): |
||
| 341 | """Get all the item's data formatted for YAML dumping.""" |
||
| 342 | data = {} |
||
| 343 | |||
| 344 | data['Is_Req'] = self._data['Is_Req'] |
||
| 345 | |||
| 346 | if data['Is_Req'] == True: |
||
| 347 | |||
| 348 | for key, value in self._data.items(): |
||
| 349 | |||
| 350 | if key == 'level': |
||
| 351 | value = value.yaml |
||
| 352 | elif key == 'text': |
||
| 353 | value = value.yaml |
||
| 354 | elif key == 'ref': |
||
| 355 | # add 11.12.2019 |
||
| 356 | value = [{str(i): i.stamp.yaml} for i in sorted(value)] #####here |
||
| 357 | #end add |
||
| 358 | elif key == 'links': |
||
| 359 | value = [{str(i): i.stamp.yaml} for i in sorted(value)] |
||
| 360 | elif key == 'reviewed': |
||
| 361 | value = value.yaml |
||
| 362 | # print(f'Value aus YAML: {value}') |
||
| 363 | else: |
||
| 364 | |||
| 365 | if isinstance(value, str): |
||
| 366 | # length of "key_text: value_text" |
||
| 367 | length = len(key) + 2 + len(value) |
||
| 368 | if length > settings.MAX_LINE_LENGTH or '\n' in value: |
||
| 369 | value = Text.save_text(value) |
||
| 370 | else: |
||
| 371 | value = str(value) # line is short enough as a strig |
||
| 372 | |||
| 373 | # add 09.12.2020 change behaviour for yaml dumping if file is not a requirement |
||
| 374 | if not key == 'Is_Req': |
||
| 375 | data[key] = value |
||
| 376 | |||
| 377 | |||
| 378 | else: |
||
| 379 | |||
| 380 | for key, value in self._data.items(): |
||
| 381 | |||
| 382 | if key == 'text': |
||
| 383 | value = value.yaml |
||
| 384 | data[key] = value |
||
| 385 | |||
| 386 | elif key == 'level': |
||
| 387 | value = value.yaml |
||
| 388 | data[key] = value |
||
| 389 | |||
| 390 | elif key == 'Is_Req': |
||
| 391 | value = to_bool(value) |
||
| 392 | data[key] = value |
||
| 393 | |||
| 394 | # Check Attribute die nicht vorhanden sind wenn item ein requirment ist |
||
| 395 | for key, value in self._NO_REQ_data.items(): |
||
| 396 | if key == 'TITLE': |
||
| 397 | length = len(key) + 2 + len(value) |
||
| 398 | if length > settings.MAX_LINE_LENGTH or '\n' in value: |
||
| 399 | value = Text.save_text(value) |
||
| 400 | else: |
||
| 401 | value = str(value) # line is short enough as a strig |
||
| 402 | |||
| 403 | data[key] = value |
||
| 404 | |||
| 405 | elif key == 'EXTENSION': |
||
| 406 | length = len(key) + 2 + len(value) |
||
| 407 | if length > settings.MAX_LINE_LENGTH or '\n' in value: |
||
| 408 | value = Text.save_text(value) |
||
| 409 | else: |
||
| 410 | value = str(value) # line is short enough as a strig |
||
| 411 | |||
| 412 | data[key] = value |
||
| 413 | |||
| 414 | elif key == 'SIDEBAR': |
||
| 415 | length = len(key) + 2 + len(value) |
||
| 416 | if length > settings.MAX_LINE_LENGTH or '\n' in value: |
||
| 417 | value = Text.save_text(value) |
||
| 418 | else: |
||
| 419 | value = str(value) # line is short enough as a strig |
||
| 420 | |||
| 421 | data[key] = value |
||
| 422 | |||
| 423 | |||
| 424 | return data |
||
| 425 | |||
| 426 | #end add |
||
| 427 | |||
| 428 | @property |
||
| 429 | def uid(self): |
||
| 430 | """Get the item's UID.""" |
||
| 431 | filename = os.path.basename(self.path) |
||
| 432 | return UID(os.path.splitext(filename)[0]) |
||
| 433 | |||
| 434 | @property |
||
| 435 | def prefix(self): |
||
| 436 | """Get the item UID's prefix.""" |
||
| 437 | return self.uid.prefix |
||
| 438 | |||
| 439 | @property |
||
| 440 | def number(self): |
||
| 441 | """Get the item UID's number.""" |
||
| 442 | return self.uid.number |
||
| 443 | |||
| 444 | @property |
||
| 445 | @auto_load |
||
| 446 | def level(self): |
||
| 447 | """Get the item's level.""" |
||
| 448 | return self._data['level'] |
||
| 449 | |||
| 450 | @level.setter |
||
| 451 | @auto_save |
||
| 452 | @auto_load |
||
| 453 | def level(self, value): |
||
| 454 | """Set the item's level.""" |
||
| 455 | self._data['level'] = Level(value) |
||
| 456 | |||
| 457 | @property |
||
| 458 | def depth(self): |
||
| 459 | """Get the item's heading order based on it's level.""" |
||
| 460 | return len(self.level) |
||
| 461 | |||
| 462 | @property |
||
| 463 | @auto_load |
||
| 464 | def active(self): |
||
| 465 | """Get the item's active status. |
||
| 466 | |||
| 467 | An inactive item will not be validated. Inactive items are |
||
| 468 | intended to be used for: |
||
| 469 | |||
| 470 | - future requirements |
||
| 471 | - temporarily disabled requirements or tests |
||
| 472 | - externally implemented requirements |
||
| 473 | - etc. |
||
| 474 | |||
| 475 | """ |
||
| 476 | return self._data['active'] |
||
| 477 | |||
| 478 | @active.setter |
||
| 479 | @auto_save |
||
| 480 | @auto_load |
||
| 481 | def active(self, value): |
||
| 482 | """Set the item's active status.""" |
||
| 483 | self._data['active'] = to_bool(value) |
||
| 484 | |||
| 485 | @property |
||
| 486 | @auto_load |
||
| 487 | def derived(self): |
||
| 488 | """Get the item's derived status. |
||
| 489 | |||
| 490 | A derived item does not have links to items in its parent |
||
| 491 | document, but should still be linked to by items in its child |
||
| 492 | documents. |
||
| 493 | |||
| 494 | """ |
||
| 495 | return self._data['derived'] |
||
| 496 | |||
| 497 | @derived.setter |
||
| 498 | @auto_save |
||
| 499 | @auto_load |
||
| 500 | def derived(self, value): |
||
| 501 | """Set the item's derived status.""" |
||
| 502 | self._data['derived'] = to_bool(value) |
||
| 503 | |||
| 504 | @property |
||
| 505 | @auto_load |
||
| 506 | def normative(self): |
||
| 507 | """Get the item's normative status. |
||
| 508 | |||
| 509 | A non-normative item should not have or be linked to. |
||
| 510 | Non-normative items are intended to be used for: |
||
| 511 | |||
| 512 | - headings |
||
| 513 | - comments |
||
| 514 | - etc. |
||
| 515 | |||
| 516 | """ |
||
| 517 | return self._data['normative'] |
||
| 518 | |||
| 519 | @normative.setter |
||
| 520 | @auto_save |
||
| 521 | @auto_load |
||
| 522 | def normative(self, value): |
||
| 523 | """Set the item's normative status.""" |
||
| 524 | self._data['normative'] = to_bool(value) |
||
| 525 | |||
| 526 | @property |
||
| 527 | def heading(self): |
||
| 528 | """Indicate if the item is a heading. |
||
| 529 | |||
| 530 | Headings have a level that ends in zero and are non-normative. |
||
| 531 | |||
| 532 | """ |
||
| 533 | return self.level.heading and not self.normative |
||
| 534 | |||
| 535 | @heading.setter |
||
| 536 | @auto_save |
||
| 537 | @auto_load |
||
| 538 | def heading(self, value): |
||
| 539 | """Set the item's heading status.""" |
||
| 540 | heading = to_bool(value) |
||
| 541 | if heading and not self.heading: |
||
| 542 | self.level.heading = True |
||
| 543 | self.normative = False |
||
| 544 | elif not heading and self.heading: |
||
| 545 | self.level.heading = False |
||
| 546 | self.normative = True |
||
| 547 | |||
| 548 | @property |
||
| 549 | @auto_load |
||
| 550 | def cleared(self): |
||
| 551 | """Indicate if no links are suspect.""" |
||
| 552 | items = self.parent_items |
||
| 553 | for uid in self.links: |
||
| 554 | for item in items: |
||
| 555 | if uid == item.uid: |
||
| 556 | if uid.stamp != item.stamp(): |
||
| 557 | return False |
||
| 558 | return True |
||
| 559 | |||
| 560 | |||
| 561 | # mabe setter and getter for reference here after implemeting clearRef function |
||
| 562 | |||
| 563 | |||
| 564 | @cleared.setter |
||
| 565 | @auto_save |
||
| 566 | @auto_load |
||
| 567 | def cleared(self, value): |
||
| 568 | """Set the item's suspect link status.""" |
||
| 569 | self.clear(_inverse=not to_bool(value)) |
||
| 570 | |||
| 571 | @property |
||
| 572 | @auto_load |
||
| 573 | def reviewed(self): |
||
| 574 | """Indicate if the item has been reviewed.""" |
||
| 575 | stamp = self.stamp(links=True) |
||
| 576 | |||
| 577 | if self._data['reviewed'] == Stamp(True): |
||
| 578 | self._data['reviewed'] = stamp |
||
| 579 | # if self.uid == "yrd-scorpos-002": |
||
| 580 | # print(stamp) |
||
| 581 | return self._data['reviewed'] == stamp |
||
| 582 | |||
| 583 | @reviewed.setter |
||
| 584 | @auto_save |
||
| 585 | @auto_load |
||
| 586 | def reviewed(self, value): |
||
| 587 | """Set the item's review status.""" |
||
| 588 | self._data['reviewed'] = Stamp(value) |
||
| 589 | |||
| 590 | @property |
||
| 591 | @auto_load |
||
| 592 | def text(self): |
||
| 593 | """Get the item's text.""" |
||
| 594 | return self._data['text'] |
||
| 595 | |||
| 596 | @text.setter |
||
| 597 | @auto_save |
||
| 598 | @auto_load |
||
| 599 | def text(self, value): |
||
| 600 | """Set the item's text.""" |
||
| 601 | self._data['text'] = Text(value) |
||
| 602 | |||
| 603 | |||
| 604 | # add 02.12.2019 |
||
| 605 | |||
| 606 | @property |
||
| 607 | @auto_load |
||
| 608 | def SPEC_RATIONALE(self): |
||
| 609 | """Get the item's SPEC_RATIONALE.""" |
||
| 610 | return self._data['SPEC_RATIONALE'] |
||
| 611 | |||
| 612 | @SPEC_RATIONALE.setter |
||
| 613 | @auto_save |
||
| 614 | @auto_load |
||
| 615 | def SPEC_RATIONALE(self, value): |
||
| 616 | """Set the item's SPEC_RATIONALE.""" |
||
| 617 | self._data['SPEC_RATIONALE'] = Text(value) |
||
| 618 | |||
| 619 | |||
| 620 | |||
| 621 | |||
| 622 | |||
| 623 | @property |
||
| 624 | @auto_load |
||
| 625 | def SPEC_SHORT_DECRIPTION(self): |
||
| 626 | """Get the item's SPEC_SHORT_DECRIPTION.""" |
||
| 627 | return self._data['SPEC_SHORT_DECRIPTION'] |
||
| 628 | |||
| 629 | @SPEC_SHORT_DECRIPTION.setter |
||
| 630 | @auto_save |
||
| 631 | @auto_load |
||
| 632 | def SPEC_SHORT_DECRIPTION(self, value): |
||
| 633 | """Set the item's SPEC_SHORT_DECRIPTION.""" |
||
| 634 | self._data['SPEC_SHORT_DECRIPTION'] = Text(value) |
||
| 635 | |||
| 636 | |||
| 637 | |||
| 638 | |||
| 639 | |||
| 640 | @property |
||
| 641 | @auto_load |
||
| 642 | def SPEC_ID(self): |
||
| 643 | """Get the item's SPEC_ID.""" |
||
| 644 | return self._data['SPEC_ID'] |
||
| 645 | |||
| 646 | @SPEC_ID.setter |
||
| 647 | @auto_save |
||
| 648 | @auto_load |
||
| 649 | def SPEC_ID(self, value): |
||
| 650 | """Set the item's SPEC_ID.""" |
||
| 651 | self._data['SPEC_ID'] = Text(value) |
||
| 652 | |||
| 653 | |||
| 654 | |||
| 655 | |||
| 656 | |||
| 657 | @property |
||
| 658 | @auto_load |
||
| 659 | def SPEC_VERSION(self): |
||
| 660 | """Get the item's SPEC_VERSION.""" |
||
| 661 | return self._data['SPEC_VERSION'] |
||
| 662 | |||
| 663 | @SPEC_VERSION.setter |
||
| 664 | @auto_save |
||
| 665 | @auto_load |
||
| 666 | def SPEC_VERSION(self, value): |
||
| 667 | """Set the item's SPEC_VERSION.""" |
||
| 668 | self._data['SPEC_VERSION'] = value |
||
| 669 | |||
| 670 | |||
| 671 | |||
| 672 | |||
| 673 | |||
| 674 | @property |
||
| 675 | @auto_load |
||
| 676 | def Assumption(self): |
||
| 677 | """Get the item's Assumption.""" |
||
| 678 | return self._data['Assumption'] |
||
| 679 | |||
| 680 | @Assumption.setter |
||
| 681 | @auto_save |
||
| 682 | @auto_load |
||
| 683 | def Assumption(self, value): |
||
| 684 | """Set the item's Assumption.""" |
||
| 685 | self._data['Assumption'] = Text(value) |
||
| 686 | |||
| 687 | |||
| 688 | |||
| 689 | |||
| 690 | |||
| 691 | @property |
||
| 692 | @auto_load |
||
| 693 | def Add_Info(self): |
||
| 694 | """Get the item's Add_Info.""" |
||
| 695 | return self._data['Add_Info'] |
||
| 696 | |||
| 697 | @Add_Info.setter |
||
| 698 | @auto_save |
||
| 699 | @auto_load |
||
| 700 | def Add_Info(self, value): |
||
| 701 | """Set the item's Add_Info.""" |
||
| 702 | self._data['Add_Info'] = Text(value) |
||
| 703 | |||
| 704 | |||
| 705 | |||
| 706 | |||
| 707 | @property |
||
| 708 | @auto_load |
||
| 709 | def Author(self): |
||
| 710 | """Get the item's Author.""" |
||
| 711 | return self._data['Author'] |
||
| 712 | |||
| 713 | @Author.setter |
||
| 714 | @auto_save |
||
| 715 | @auto_load |
||
| 716 | def Author(self, value): |
||
| 717 | """Set the item's Author.""" |
||
| 718 | self._data['Author'] = Text(value) |
||
| 719 | |||
| 720 | |||
| 721 | |||
| 722 | |||
| 723 | @property |
||
| 724 | @auto_load |
||
| 725 | def Generation(self): |
||
| 726 | """Get the item's Generation.""" |
||
| 727 | return self._data['Generation'] |
||
| 728 | |||
| 729 | @Generation.setter |
||
| 730 | @auto_save |
||
| 731 | @auto_load |
||
| 732 | def Generation(self, value): |
||
| 733 | """Set the item's Generation.""" |
||
| 734 | self._data['Generation'] = Text(value) |
||
| 735 | |||
| 736 | |||
| 737 | |||
| 738 | |||
| 739 | |||
| 740 | @property |
||
| 741 | @auto_load |
||
| 742 | def REFINE(self): |
||
| 743 | """Get the item's REFINE.""" |
||
| 744 | return self._data['REFINE'] |
||
| 745 | |||
| 746 | @REFINE.setter |
||
| 747 | @auto_save |
||
| 748 | @auto_load |
||
| 749 | def REFINE(self, value): |
||
| 750 | """Set the item's REFINE.""" |
||
| 751 | self._data['REFINE'] = Text(value) |
||
| 752 | |||
| 753 | |||
| 754 | |||
| 755 | |||
| 756 | @property |
||
| 757 | @auto_load |
||
| 758 | def ASSOCIATED(self): |
||
| 759 | """Get the item's ASSOCIATED.""" |
||
| 760 | return self._data['ASSOCIATED'] |
||
| 761 | |||
| 762 | @ASSOCIATED.setter |
||
| 763 | @auto_save |
||
| 764 | @auto_load |
||
| 765 | def ASSOCIATED(self, value): |
||
| 766 | """Set the item's ASSOCIATED.""" |
||
| 767 | self._data['ASSOCIATED'] = Text(value) |
||
| 768 | |||
| 769 | |||
| 770 | |||
| 771 | |||
| 772 | @property |
||
| 773 | @auto_load |
||
| 774 | def SPEC_STATUS(self): |
||
| 775 | """Get the item's SPEC_STATUS.""" |
||
| 776 | return self._data['SPEC_STATUS'] |
||
| 777 | |||
| 778 | @SPEC_STATUS.setter |
||
| 779 | @auto_save |
||
| 780 | @auto_load |
||
| 781 | def SPEC_STATUS(self, value): |
||
| 782 | """Set the item's SPEC_STATUS.""" |
||
| 783 | self._data['SPEC_STATUS'] = Text(value) |
||
| 784 | |||
| 785 | |||
| 786 | |||
| 787 | |||
| 788 | |||
| 789 | @property |
||
| 790 | @auto_load |
||
| 791 | def Validation_Ref(self): |
||
| 792 | """Get the item's Validation_Ref.""" |
||
| 793 | return self._data['Validation_Ref'] |
||
| 794 | |||
| 795 | @Validation_Ref.setter |
||
| 796 | @auto_save |
||
| 797 | @auto_load |
||
| 798 | def Validation_Ref(self, value): |
||
| 799 | """Set the item's Validation_Ref.""" |
||
| 800 | self._data['Validation_Ref'] = Text(value) |
||
| 801 | |||
| 802 | |||
| 803 | |||
| 804 | |||
| 805 | @property |
||
| 806 | @auto_load |
||
| 807 | def Validation_Mean(self): |
||
| 808 | """Get the item's Validation_Mean.""" |
||
| 809 | return self._data['Validation_Mean'] |
||
| 810 | |||
| 811 | @Validation_Mean.setter |
||
| 812 | @auto_save |
||
| 813 | @auto_load |
||
| 814 | def Validation_Mean(self, value): |
||
| 815 | """Set the item's Validation_Mean.""" |
||
| 816 | self._data['Validation_Mean'] = Text(value) |
||
| 817 | |||
| 818 | |||
| 819 | |||
| 820 | |||
| 821 | @property |
||
| 822 | @auto_load |
||
| 823 | def Verification_Ref(self): |
||
| 824 | """Get the item's Verification_Ref.""" |
||
| 825 | return self._data['Verification_Ref'] |
||
| 826 | |||
| 827 | @Verification_Ref.setter |
||
| 828 | @auto_save |
||
| 829 | @auto_load |
||
| 830 | def Verification_Ref(self, value): |
||
| 831 | """Set the item's Verification_Ref.""" |
||
| 832 | self._data['Verification_Ref'] = Text(value) |
||
| 833 | |||
| 834 | |||
| 835 | |||
| 836 | |||
| 837 | @property |
||
| 838 | @auto_load |
||
| 839 | def Verification_Mean(self): |
||
| 840 | """Get the item's Verification_Mean.""" |
||
| 841 | return self._data['Verification_Mean'] |
||
| 842 | |||
| 843 | @Verification_Mean.setter |
||
| 844 | @auto_save |
||
| 845 | @auto_load |
||
| 846 | def Verification_Mean(self, value): |
||
| 847 | """Set the item's Verification_Mean.""" |
||
| 848 | self._data['Verification_Mean'] = Text(value) |
||
| 849 | |||
| 850 | |||
| 851 | |||
| 852 | |||
| 853 | |||
| 854 | @property |
||
| 855 | @auto_load |
||
| 856 | def Verification_Rationale(self): |
||
| 857 | """Get the item's Verification_Rationale.""" |
||
| 858 | return self._data['Verification_Rationale'] |
||
| 859 | |||
| 860 | @Verification_Rationale.setter |
||
| 861 | @auto_save |
||
| 862 | @auto_load |
||
| 863 | def Verification_Rationale(self, value): |
||
| 864 | """Set the item's Verification_Rationale.""" |
||
| 865 | self._data['Verification_Rationale'] = Text(value) |
||
| 866 | |||
| 867 | |||
| 868 | |||
| 869 | |||
| 870 | |||
| 871 | @property |
||
| 872 | @auto_load |
||
| 873 | def Verification_Status(self): |
||
| 874 | """Get the item's Verification_Status.""" |
||
| 875 | return self._data['Verification_Status'] |
||
| 876 | |||
| 877 | @Verification_Status.setter |
||
| 878 | @auto_save |
||
| 879 | @auto_load |
||
| 880 | def Verification_Status(self, value): |
||
| 881 | """Set the item's Verification_Status.""" |
||
| 882 | self._data['Verification_Status'] = Text(value) |
||
| 883 | |||
| 884 | |||
| 885 | |||
| 886 | |||
| 887 | |||
| 888 | @property |
||
| 889 | @auto_load |
||
| 890 | def VoV_Ref(self): |
||
| 891 | """Get the item's VoV_Ref.""" |
||
| 892 | return self._data['VoV_Ref'] |
||
| 893 | |||
| 894 | @VoV_Ref.setter |
||
| 895 | @auto_save |
||
| 896 | @auto_load |
||
| 897 | def VoV_Ref(self, value): |
||
| 898 | """Set the item's VoV_Ref.""" |
||
| 899 | self._data['VoV_Ref'] = Text(value) |
||
| 900 | |||
| 901 | |||
| 902 | |||
| 903 | |||
| 904 | |||
| 905 | @property |
||
| 906 | @auto_load |
||
| 907 | def Allocation(self): |
||
| 908 | """Get the item's Allocation.""" |
||
| 909 | return self._data['Allocation'] |
||
| 910 | |||
| 911 | @Allocation.setter |
||
| 912 | @auto_save |
||
| 913 | @auto_load |
||
| 914 | def Allocation(self, value): |
||
| 915 | """Set the item's Allocation.""" |
||
| 916 | self._data['Allocation'] = Text(value) |
||
| 917 | |||
| 918 | |||
| 919 | |||
| 920 | |||
| 921 | @property |
||
| 922 | @auto_load |
||
| 923 | def SPEC_STATUS(self): |
||
| 924 | """Get the item's SPEC_STATUS.""" |
||
| 925 | return self._data['SPEC_STATUS'] |
||
| 926 | |||
| 927 | @SPEC_STATUS.setter |
||
| 928 | @auto_save |
||
| 929 | @auto_load |
||
| 930 | def SPEC_STATUS(self, value): |
||
| 931 | """Set the item's SPEC_STATUS.""" |
||
| 932 | self._data['SPEC_STATUS'] = Text(value) |
||
| 933 | |||
| 934 | |||
| 935 | # end add |
||
| 936 | |||
| 937 | # Is_Req add 09.01.2020 |
||
| 938 | |||
| 939 | @property |
||
| 940 | @auto_load |
||
| 941 | def Is_Req(self): |
||
| 942 | """Get the items Is_Req boolean.""" |
||
| 943 | return self._data['Is_Req'] |
||
| 944 | |||
| 945 | @Is_Req.setter |
||
| 946 | @auto_save |
||
| 947 | @auto_load |
||
| 948 | def Is_Req(self, value): |
||
| 949 | """Set the item's Is_Req boolean.""" |
||
| 950 | self._data['Is_Req'] = to_bool(value) |
||
| 951 | |||
| 952 | # end Is_Req add |
||
| 953 | |||
| 954 | # add 16.01.2020 |
||
| 955 | # Es werden die attribute hinzugefügt die nur erscheinen wenn es sich nicht um ein Requirment handelt |
||
| 956 | |||
| 957 | @property |
||
| 958 | @auto_load |
||
| 959 | def EXTENSION(self): |
||
| 960 | """Get the items EXTENSION string.""" |
||
| 961 | return self._NO_REQ_data['EXTENSION'] |
||
| 962 | |||
| 963 | @EXTENSION.setter |
||
| 964 | @auto_save |
||
| 965 | @auto_load |
||
| 966 | def EXTENSION(self, value): |
||
| 967 | """Set the item's EXTENSION value.""" |
||
| 968 | self._NO_REQ_data['EXTENSION'] = Text(value) |
||
| 969 | |||
| 970 | |||
| 971 | |||
| 972 | @property |
||
| 973 | @auto_load |
||
| 974 | def TITLE(self): |
||
| 975 | """Get the items TITLE string.""" |
||
| 976 | return self._NO_REQ_data['TITLE'] |
||
| 977 | |||
| 978 | @TITLE.setter |
||
| 979 | @auto_save |
||
| 980 | @auto_load |
||
| 981 | def TITLE(self, value): |
||
| 982 | """Set the item's TITLE value.""" |
||
| 983 | self._NO_REQ_data['TITLE'] = Text(value) |
||
| 984 | |||
| 985 | |||
| 986 | |||
| 987 | @property |
||
| 988 | @auto_load |
||
| 989 | def SIDEBAR(self): |
||
| 990 | """Get the items SIDEBAR string.""" |
||
| 991 | return self._NO_REQ_data['SIDEBAR'] |
||
| 992 | |||
| 993 | @SIDEBAR.setter |
||
| 994 | @auto_save |
||
| 995 | @auto_load |
||
| 996 | def SIDEBAR(self, value): |
||
| 997 | """Set the item's SIDEBAR value.""" |
||
| 998 | self._NO_REQ_data['SIDEBAR'] = Text(value) |
||
| 999 | |||
| 1000 | |||
| 1001 | # add 10.12.2019 |
||
| 1002 | @property |
||
| 1003 | @auto_load |
||
| 1004 | def ref(self): |
||
| 1005 | """Get the item's external file reference. |
||
| 1006 | |||
| 1007 | An external reference can be part of a line in a text file or |
||
| 1008 | the filename of any type of file. |
||
| 1009 | |||
| 1010 | """ |
||
| 1011 | return sorted(self._data['ref']) |
||
| 1012 | |||
| 1013 | @ref.setter |
||
| 1014 | @auto_save |
||
| 1015 | @auto_load |
||
| 1016 | def ref(self, value): |
||
| 1017 | """Set the item's external file reference.""" |
||
| 1018 | self._data['ref'] = set(REF(v) for v in value) |
||
| 1019 | |||
| 1020 | # end add |
||
| 1021 | |||
| 1022 | @property |
||
| 1023 | @auto_load |
||
| 1024 | def links(self): |
||
| 1025 | """Get a list of the item UIDs this item links to.""" |
||
| 1026 | return sorted(self._data['links']) |
||
| 1027 | |||
| 1028 | @links.setter |
||
| 1029 | @auto_save |
||
| 1030 | @auto_load |
||
| 1031 | def links(self, value): |
||
| 1032 | """Set the list of item UIDs this item links to.""" |
||
| 1033 | self._data['links'] = set(UID(v) for v in value) |
||
| 1034 | |||
| 1035 | @property |
||
| 1036 | def parent_links(self): |
||
| 1037 | """Get a list of the item UIDs this item links to.""" |
||
| 1038 | return self.links # alias |
||
| 1039 | |||
| 1040 | @parent_links.setter |
||
| 1041 | def parent_links(self, value): |
||
| 1042 | """Set the list of item UIDs this item links to.""" |
||
| 1043 | self.links = value # alias |
||
| 1044 | |||
| 1045 | @property |
||
| 1046 | @requires_tree |
||
| 1047 | def parent_items(self): |
||
| 1048 | """Get a list of items that this item links to.""" |
||
| 1049 | items = [] |
||
| 1050 | for uid in self.links: |
||
| 1051 | try: |
||
| 1052 | item = self.tree.find_item(uid) |
||
| 1053 | except DoorstopError: |
||
| 1054 | item = UnknownItem(uid) |
||
| 1055 | log.warning(item.exception) |
||
| 1056 | items.append(item) |
||
| 1057 | return items |
||
| 1058 | |||
| 1059 | @property |
||
| 1060 | @requires_tree |
||
| 1061 | @requires_document |
||
| 1062 | def parent_documents(self): |
||
| 1063 | """Get a list of documents that this item's document should link to. |
||
| 1064 | |||
| 1065 | .. note:: |
||
| 1066 | |||
| 1067 | A document only has one parent. |
||
| 1068 | |||
| 1069 | """ |
||
| 1070 | try: |
||
| 1071 | return [self.tree.find_document(self.document.prefix)] |
||
| 1072 | except DoorstopError: |
||
| 1073 | log.warning(Prefix.UNKNOWN_MESSGE.format(self.document.prefix)) |
||
| 1074 | return [] |
||
| 1075 | |||
| 1076 | # actions ################################################################ |
||
| 1077 | |||
| 1078 | @auto_save |
||
| 1079 | def edit(self, tool=None): |
||
| 1080 | """Open the item for editing. |
||
| 1081 | |||
| 1082 | :param tool: path of alternate editor |
||
| 1083 | |||
| 1084 | """ |
||
| 1085 | # Lock the item |
||
| 1086 | if self.tree: |
||
| 1087 | self.tree.vcs.lock(self.path) |
||
| 1088 | # Open in an editor |
||
| 1089 | editor.edit(self.path, tool=tool) |
||
| 1090 | # Force reloaded |
||
| 1091 | self._loaded = False |
||
| 1092 | |||
| 1093 | @auto_save |
||
| 1094 | @auto_load |
||
| 1095 | def link(self, value): |
||
| 1096 | """Add a new link to another item UID. |
||
| 1097 | |||
| 1098 | :param value: item or UID |
||
| 1099 | |||
| 1100 | """ |
||
| 1101 | uid = UID(value) |
||
| 1102 | log.info("linking to '{}'...".format(uid)) |
||
| 1103 | self._data['links'].add(uid) |
||
| 1104 | |||
| 1105 | # add 13.12.2019 |
||
| 1106 | @auto_save |
||
| 1107 | @auto_load |
||
| 1108 | def add_ref(self,value): |
||
| 1109 | """Add new reference to Item |
||
| 1110 | :param value: item or UID |
||
| 1111 | |||
| 1112 | """ |
||
| 1113 | ref = REF(value) |
||
| 1114 | log.info("referencing to'{}'...".format(ref)) |
||
| 1115 | self._data['ref'].add(ref) |
||
| 1116 | |||
| 1117 | # end add |
||
| 1118 | |||
| 1119 | |||
| 1120 | |||
| 1121 | @auto_save |
||
| 1122 | @auto_load |
||
| 1123 | def unlink(self, value): |
||
| 1124 | """Remove an existing link by item UID. |
||
| 1125 | |||
| 1126 | :param value: item or UID |
||
| 1127 | |||
| 1128 | """ |
||
| 1129 | uid = UID(value) |
||
| 1130 | try: |
||
| 1131 | self._data['links'].remove(uid) |
||
| 1132 | except KeyError: |
||
| 1133 | log.warning("link to {0} does not exist".format(uid)) |
||
| 1134 | |||
| 1135 | def get_issues(self, **kwargs): |
||
| 1136 | """Yield all the item's issues. |
||
| 1137 | |||
| 1138 | :return: generator of :class:`~doorstop.common.DoorstopError`, |
||
| 1139 | :class:`~doorstop.common.DoorstopWarning`, |
||
| 1140 | :class:`~doorstop.common.DoorstopInfo` |
||
| 1141 | |||
| 1142 | """ |
||
| 1143 | |||
| 1144 | |||
| 1145 | assert kwargs.get('document_hook') is None |
||
| 1146 | assert kwargs.get('item_hook') is None |
||
| 1147 | log.info("checking item {}...".format(self)) |
||
| 1148 | # Verify the file can be parsed |
||
| 1149 | self.load() |
||
| 1150 | |||
| 1151 | # add 09.01.2020 -> If item is not a requirement do nothing |
||
| 1152 | if self._data['Is_Req'] == False: |
||
| 1153 | self.save() |
||
| 1154 | if not self.text: |
||
| 1155 | yield DoorstopWarning("no text") |
||
| 1156 | return |
||
| 1157 | # Skip inactive items |
||
| 1158 | if not self.active: |
||
| 1159 | log.info("skipped inactive item: {}".format(self)) |
||
| 1160 | return |
||
| 1161 | # Delay item save if reformatting |
||
| 1162 | if settings.REFORMAT: |
||
| 1163 | self.auto = False |
||
| 1164 | # Check text |
||
| 1165 | if not self.text: |
||
| 1166 | yield DoorstopWarning("no text") |
||
| 1167 | # Check external references |
||
| 1168 | if settings.CHECK_REF: |
||
| 1169 | try: |
||
| 1170 | self.find_ref() |
||
| 1171 | except DoorstopError as exc: |
||
| 1172 | yield exc |
||
| 1173 | # Check links |
||
| 1174 | if not self.normative and self.links: |
||
| 1175 | yield DoorstopWarning("non-normative, but has links") |
||
| 1176 | # Check links against the document |
||
| 1177 | if self.document: |
||
| 1178 | yield from self._get_issues_document(self.document) |
||
| 1179 | # Check links against the tree |
||
| 1180 | if self.tree: |
||
| 1181 | yield from self._get_issues_tree(self.tree) |
||
| 1182 | # Check links against both document and tree |
||
| 1183 | if self.document and self.tree: |
||
| 1184 | yield from self._get_issues_both(self.document, self.tree) |
||
| 1185 | # Check review status |
||
| 1186 | # print('REVIEWD:') |
||
| 1187 | # print(self.reviewed) |
||
| 1188 | if not self.reviewed: |
||
| 1189 | if settings.CHECK_REVIEW_STATUS: |
||
| 1190 | # print('HALLO') |
||
| 1191 | yield DoorstopWarning("unreviewed changes") |
||
| 1192 | # Reformat the file |
||
| 1193 | if settings.REFORMAT: |
||
| 1194 | log.debug("reformatting item {}...".format(self)) |
||
| 1195 | self.save() |
||
| 1196 | |||
| 1197 | # add 13.12.2019 |
||
| 1198 | |||
| 1199 | # check ob references inaktiv gesetzt werden müssen |
||
| 1200 | for ref in self.ref: |
||
| 1201 | # falls stamp noch keinen wert hat bekommt er hier den aktuellen |
||
| 1202 | if ref.stamp != self.stamp(ID=ref.value): |
||
| 1203 | if settings.CHECK_REF_STATUS: |
||
| 1204 | yield DoorstopWarning('suspect reference {}'.format(ref)) |
||
| 1205 | |||
| 1206 | # print(self) |
||
| 1207 | # print(self.ref) |
||
| 1208 | |||
| 1209 | |||
| 1210 | # end add |
||
| 1211 | |||
| 1212 | def _get_issues_document(self, document): |
||
| 1213 | """Yield all the item's issues against its document.""" |
||
| 1214 | log.debug("getting issues against document...") |
||
| 1215 | # Verify an item's UID matches its document's prefix |
||
| 1216 | if self.prefix != document.prefix: |
||
| 1217 | msg = "prefix differs from document ({})".format(document.prefix) |
||
| 1218 | yield DoorstopInfo(msg) |
||
| 1219 | # Verify an item has upward links |
||
| 1220 | if all((document.parent, |
||
| 1221 | self.normative, |
||
| 1222 | not self.derived)) and not self.links: |
||
| 1223 | msg = "no links to parent document: {}".format(document.parent) |
||
| 1224 | yield DoorstopWarning(msg) |
||
| 1225 | # Verify an item's links are to the correct parent |
||
| 1226 | for uid in self.links: |
||
| 1227 | try: |
||
| 1228 | prefix = uid.prefix |
||
| 1229 | except DoorstopError: |
||
| 1230 | msg = "invalid UID in links: {}".format(uid) |
||
| 1231 | yield DoorstopError(msg) |
||
| 1232 | else: |
||
| 1233 | if document.parent and prefix != document.parent: |
||
| 1234 | # this is only 'info' because a document is allowed |
||
| 1235 | # to contain items with a different prefix, but |
||
| 1236 | # Doorstop will not create items like this |
||
| 1237 | msg = "parent is '{}', but linked to: {}".format( |
||
| 1238 | document.parent, uid) |
||
| 1239 | yield DoorstopInfo(msg) |
||
| 1240 | |||
| 1241 | View Code Duplication | def _get_issues_tree(self, tree): |
|
| 1242 | """Yield all the item's issues against its tree.""" |
||
| 1243 | log.debug("getting issues against tree...") |
||
| 1244 | # Verify an item's links are valid |
||
| 1245 | |||
| 1246 | identifiers = set() |
||
| 1247 | for uid in self.links: |
||
| 1248 | try: |
||
| 1249 | item = tree.find_item(uid) |
||
| 1250 | except DoorstopError: |
||
| 1251 | identifiers.add(uid) # keep the invalid UID |
||
| 1252 | msg = "linked to unknown item: {}".format(uid) |
||
| 1253 | yield DoorstopError(msg) |
||
| 1254 | else: |
||
| 1255 | # check the linked item |
||
| 1256 | if not item.active: |
||
| 1257 | msg = "linked to inactive item: {}".format(item) |
||
| 1258 | yield DoorstopInfo(msg) |
||
| 1259 | if not item.normative: |
||
| 1260 | msg = "linked to non-normative item: {}".format(item) |
||
| 1261 | yield DoorstopWarning(msg) |
||
| 1262 | # check the link status |
||
| 1263 | |||
| 1264 | # wenn bis es bis jetzt noch keinen stamp gibt ... funktioniert allerdings glaube ich nicht .... |
||
| 1265 | if uid.stamp == Stamp(True): |
||
| 1266 | uid.stamp = item.stamp() #... convert True to a stamp |
||
| 1267 | |||
| 1268 | |||
| 1269 | elif uid.stamp != item.stamp(): |
||
| 1270 | # 12.12.2019 |
||
| 1271 | # print('STAMPS FOR LINKS') |
||
| 1272 | # print(item.stamp()) |
||
| 1273 | # print(uid.stamp) |
||
| 1274 | # print(uid) |
||
| 1275 | if settings.CHECK_SUSPECT_LINKS: |
||
| 1276 | msg = "suspect link: {}".format(item) |
||
| 1277 | yield DoorstopWarning(msg) |
||
| 1278 | # reformat the item's UID |
||
| 1279 | identifier2 = UID(item.uid, stamp=uid.stamp) |
||
| 1280 | identifiers.add(identifier2) |
||
| 1281 | # Apply the reformatted item UIDs |
||
| 1282 | if settings.REFORMAT: |
||
| 1283 | self._data['links'] = identifiers |
||
| 1284 | |||
| 1285 | def _get_issues_both(self, document, tree): |
||
| 1286 | """Yield all the item's issues against its document and tree.""" |
||
| 1287 | log.debug("getting issues against document and tree...") |
||
| 1288 | # Verify an item is being linked to (child links) |
||
| 1289 | if settings.CHECK_CHILD_LINKS and self.normative: |
||
| 1290 | items, documents = self._find_child_objects(document=document, |
||
| 1291 | tree=tree, |
||
| 1292 | find_all=False) |
||
| 1293 | if not items: |
||
| 1294 | for document in documents: |
||
| 1295 | msg = "no links from child document: {}".format(document) |
||
| 1296 | yield DoorstopWarning(msg) |
||
| 1297 | |||
| 1298 | |||
| 1299 | |||
| 1300 | # add 11.12.2019 umbau in einen for loop |
||
| 1301 | |||
| 1302 | @requires_tree |
||
| 1303 | def find_ref(self, paths=False): |
||
| 1304 | """Get the external file reference and line number. |
||
| 1305 | |||
| 1306 | :raises: :class:`~doorstop.common.DoorstopError` when no |
||
| 1307 | reference is found |
||
| 1308 | |||
| 1309 | :return: relative path to file or None (when no reference |
||
| 1310 | set), |
||
| 1311 | line number (when found in file) or None (when found as |
||
| 1312 | filename) or None (when no reference set) |
||
| 1313 | |||
| 1314 | """ |
||
| 1315 | # add 06.01.2020 |
||
| 1316 | # Choose generator to iterate over: |
||
| 1317 | if paths: |
||
| 1318 | iterator = paths |
||
| 1319 | else: |
||
| 1320 | iterator = self.tree.vcs.paths |
||
| 1321 | # end add |
||
| 1322 | # Return immediately if no external reference |
||
| 1323 | if not self.ref: |
||
| 1324 | log.debug("no external reference to search for") |
||
| 1325 | return None, None |
||
| 1326 | # Update the cache |
||
| 1327 | if not settings.CACHE_PATHS: |
||
| 1328 | pyficache.clear_file_cache() |
||
| 1329 | # Search for the external reference |
||
| 1330 | # initialiseren zweier listen die mit den referencen gefüllt werden |
||
| 1331 | ref_found = {} |
||
| 1332 | |||
| 1333 | ### hier umbau in loop // self.ref wird überall mit ref.value ersetzt um zugriff auf die einzelnen Strings zu haben ! |
||
| 1334 | for ref in self.ref: |
||
| 1335 | |||
| 1336 | # hinzufügen eines dictionaries mit true == gefunden und false == nicht gefunden -> tracking der gefundenen referencen im code |
||
| 1337 | ref_found[ref.value] = False |
||
| 1338 | |||
| 1339 | |||
| 1340 | log.debug("seraching for ref '{}'...".format(ref.value)) |
||
| 1341 | pattern = r"(\b|\W){}(\b|\W)".format(re.escape(ref.value)) |
||
| 1342 | log.trace("regex: {}".format(pattern)) |
||
| 1343 | regex = re.compile(pattern) |
||
| 1344 | for path, filename, relpath in iterator: |
||
| 1345 | # Skip the item's file while searching |
||
| 1346 | if path == self.path: |
||
| 1347 | # pfad zur yaml datei in der referencen stehen ... |
||
| 1348 | continue |
||
| 1349 | # Check for a matching filename |
||
| 1350 | if filename == ref.value: |
||
| 1351 | return relpath, 'Directory' |
||
| 1352 | # Skip extensions that should not be considered text |
||
| 1353 | if os.path.splitext(filename)[-1] in settings.SKIP_EXTS: |
||
| 1354 | continue |
||
| 1355 | # Search for the reference in the file |
||
| 1356 | # add 11.12.2019 // fill lists added before // 16.12.2019 --> dictionary mit false als value |
||
| 1357 | # wenn nicht gefunden ansonsten tuple mit file/line |
||
| 1358 | lines = pyficache.getlines(path) |
||
| 1359 | if lines is None: |
||
| 1360 | log.trace("unable to read lines from: {}".format(path)) |
||
| 1361 | continue |
||
| 1362 | |||
| 1363 | for lineno, line in enumerate(lines, start=1): |
||
| 1364 | if regex.search(line): |
||
| 1365 | log.debug("found ref: {}".format(relpath)) |
||
| 1366 | if ref_found[ref.value] == False: |
||
| 1367 | ref_found[ref.value] = (relpath, lineno) |
||
| 1368 | else: |
||
| 1369 | msg = f"found aaaa multiple times same refernce in code!\n=>('{relpath}', {lineno}) and {ref_found[ref.value]}" |
||
| 1370 | raise DoorstopError(msg) |
||
| 1371 | |||
| 1372 | if ref_found: |
||
| 1373 | return ref_found |
||
| 1374 | else: |
||
| 1375 | return None |
||
| 1376 | # end add |
||
| 1377 | |||
| 1378 | |||
| 1379 | # end add |
||
| 1380 | |||
| 1381 | |||
| 1382 | |||
| 1383 | |||
| 1384 | |||
| 1385 | |||
| 1386 | def find_child_links(self, find_all=True): |
||
| 1387 | """Get a list of item UIDs that link to this item (reverse links). |
||
| 1388 | |||
| 1389 | :param find_all: find all items (not just the first) before returning |
||
| 1390 | |||
| 1391 | :return: list of found item UIDs |
||
| 1392 | |||
| 1393 | """ |
||
| 1394 | items, _ = self._find_child_objects(find_all=find_all) |
||
| 1395 | identifiers = [item.uid for item in items] |
||
| 1396 | return identifiers |
||
| 1397 | |||
| 1398 | child_links = property(find_child_links) |
||
| 1399 | |||
| 1400 | def find_child_items(self, find_all=True): |
||
| 1401 | """Get a list of items that link to this item. |
||
| 1402 | |||
| 1403 | :param find_all: find all items (not just the first) before returning |
||
| 1404 | |||
| 1405 | :return: list of found items |
||
| 1406 | |||
| 1407 | """ |
||
| 1408 | items, _ = self._find_child_objects(find_all=find_all) |
||
| 1409 | return items |
||
| 1410 | |||
| 1411 | child_items = property(find_child_items) |
||
| 1412 | |||
| 1413 | def find_child_documents(self): |
||
| 1414 | """Get a list of documents that should link to this item's document. |
||
| 1415 | |||
| 1416 | :return: list of found documents |
||
| 1417 | |||
| 1418 | """ |
||
| 1419 | _, documents = self._find_child_objects(find_all=False) |
||
| 1420 | return documents |
||
| 1421 | |||
| 1422 | child_documents = property(find_child_documents) |
||
| 1423 | |||
| 1424 | def _find_child_objects(self, document=None, tree=None, find_all=True): |
||
| 1425 | """Get lists of child items and child documents. |
||
| 1426 | |||
| 1427 | :param document: document containing the current item |
||
| 1428 | :param tree: tree containing the current item |
||
| 1429 | :param find_all: find all items (not just the first) before returning |
||
| 1430 | |||
| 1431 | :return: list of found items, list of all child documents |
||
| 1432 | |||
| 1433 | """ |
||
| 1434 | child_items = [] |
||
| 1435 | child_documents = [] |
||
| 1436 | document = document or self.document |
||
| 1437 | tree = tree or self.tree |
||
| 1438 | if not document or not tree: |
||
| 1439 | return child_items, child_documents |
||
| 1440 | # Find child objects |
||
| 1441 | log.debug("finding item {}'s child objects...".format(self)) |
||
| 1442 | for document2 in tree: |
||
| 1443 | if document2.parent == document.prefix: |
||
| 1444 | child_documents.append(document2) |
||
| 1445 | # Search for child items unless we only need to find one |
||
| 1446 | if not child_items or find_all: |
||
| 1447 | for item2 in document2: |
||
| 1448 | if self.uid in item2.links: |
||
| 1449 | child_items.append(item2) |
||
| 1450 | if not find_all: |
||
| 1451 | break |
||
| 1452 | # Display found links |
||
| 1453 | if child_items: |
||
| 1454 | if find_all: |
||
| 1455 | joined = ', '.join(str(i) for i in child_items) |
||
| 1456 | msg = "child items: {}".format(joined) |
||
| 1457 | else: |
||
| 1458 | msg = "first child item: {}".format(child_items[0]) |
||
| 1459 | log.debug(msg) |
||
| 1460 | joined = ', '.join(str(d) for d in child_documents) |
||
| 1461 | log.debug("child documents: {}".format(joined)) |
||
| 1462 | return sorted(child_items), child_documents |
||
| 1463 | |||
| 1464 | @auto_load |
||
| 1465 | def stamp(self, links=False, ID=False): |
||
| 1466 | """Hash the item's key content for later comparison.""" |
||
| 1467 | # add 15.12.2019 values werden nach ID Parameter ausgewählt |
||
| 1468 | if not ID: |
||
| 1469 | values = [self.uid, self.text, self.Assumption] #self.ref] reference nicht mit reinnhemen weil sie siich verändert bei clear ref |
||
| 1470 | # un damit das säubern reihenfolgenahbhängig wird |
||
| 1471 | |||
| 1472 | |||
| 1473 | if links: |
||
| 1474 | values.extend(self.links) |
||
| 1475 | |||
| 1476 | |||
| 1477 | |||
| 1478 | else: |
||
| 1479 | |||
| 1480 | # end add |
||
| 1481 | values = [self.text,self.Assumption, ID] |
||
| 1482 | # print(f'VALUES:{values}') |
||
| 1483 | |||
| 1484 | return Stamp(*values) |
||
| 1485 | |||
| 1486 | @auto_save |
||
| 1487 | @auto_load |
||
| 1488 | def clear(self, _inverse=False): |
||
| 1489 | """Clear suspect links.""" |
||
| 1490 | log.info("clearing suspect links...") |
||
| 1491 | items = self.parent_items |
||
| 1492 | for uid in self.links: |
||
| 1493 | for item in items: |
||
| 1494 | if uid == item.uid: |
||
| 1495 | if _inverse: |
||
| 1496 | uid.stamp = Stamp() |
||
| 1497 | else: |
||
| 1498 | uid.stamp = item.stamp() |
||
| 1499 | |||
| 1500 | |||
| 1501 | @auto_save |
||
| 1502 | @auto_load |
||
| 1503 | def review(self): |
||
| 1504 | """Mark the item as reviewed.""" |
||
| 1505 | log.info("marking item as reviewed...") |
||
| 1506 | self._data['reviewed'] = self.stamp(links=True) |
||
| 1507 | |||
| 1508 | |||
| 1509 | |||
| 1510 | # add 15.12.2019 |
||
| 1511 | @auto_save |
||
| 1512 | @auto_load |
||
| 1513 | def clearRef(self, referenceID): |
||
| 1514 | """change suspect refernece status""" |
||
| 1515 | log.info("marking reference as reviewed...") |
||
| 1516 | # Mit found_id wird sichergestellt, dass auch eine ID gefunden wurd |
||
| 1517 | # ansonsten wir fehler ausgegen das die angegebne ID nicht gefunden werdne kann.. |
||
| 1518 | found_id = False |
||
| 1519 | if referenceID == 'all': |
||
| 1520 | found_id = True |
||
| 1521 | for ref in self.ref: |
||
| 1522 | # item aktuellen stamp aus spezifischer id und text geben |
||
| 1523 | ref.stamp = self.stamp(ID=ref.value) |
||
| 1524 | |||
| 1525 | else: |
||
| 1526 | for ref in self.ref: |
||
| 1527 | if referenceID == ref.value: |
||
| 1528 | found_id = True |
||
| 1529 | ref.stamp = self.stamp(ID=ref.value) |
||
| 1530 | |||
| 1531 | if not found_id: |
||
| 1532 | raise DoorstopError("ID does not exist: {}".format(referenceID)) |
||
| 1533 | |||
| 1534 | |||
| 1535 | # end add |
||
| 1536 | |||
| 1537 | @delete_item |
||
| 1538 | def delete(self, path=None): |
||
| 1539 | """Delete the item.""" |
||
| 1540 | pass # the item is deleted in the decorated method |
||
| 1541 | |||
| 1542 | |||
| 1543 | |||
| 1544 | class UnknownItem(object): |
||
| 1545 | |||
| 1546 | """Represents an unknown item, which doesn't have a path.""" |
||
| 1547 | |||
| 1548 | UNKNOWN_PATH = '???' # string to represent an unknown path |
||
| 1549 | |||
| 1550 | normative = False # do not include unknown items in traceability |
||
| 1551 | |||
| 1552 | def __init__(self, value, spec=Item): |
||
| 1553 | self._uid = UID(value) |
||
| 1554 | self._spec = dir(spec) # list of attribute names for warnings |
||
| 1555 | msg = UID.UNKNOWN_MESSAGE.format(k='', u=self.uid) |
||
| 1556 | self.exception = DoorstopError(msg) |
||
| 1557 | |||
| 1558 | def __str__(self): |
||
| 1559 | return Item.__str__(self) |
||
| 1560 | |||
| 1561 | def __getattr__(self, name): |
||
| 1562 | if name in self._spec: |
||
| 1563 | log.debug(self.exception) |
||
| 1564 | return self.__getattribute__(name) |
||
| 1565 | |||
| 1566 | @property |
||
| 1567 | def uid(self): |
||
| 1568 | """Get the item's UID.""" |
||
| 1569 | return self._uid |
||
| 1570 | |||
| 1571 | prefix = Item.prefix |
||
| 1572 | number = Item.number |
||
| 1573 | |||
| 1574 | @property |
||
| 1575 | def relpath(self): |
||
| 1576 | """Get the unknown item's relative path string.""" |
||
| 1577 | return "@{}???".format(os.sep, self.UNKNOWN_PATH) |
||
| 1578 | |||
| 1579 | def stamp(self): # pylint: disable=R0201 |
||
| 1580 | """Return an empty stamp.""" |
||
| 1581 | return Stamp(None) |
||
| 1582 |