Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation concept for "mapped" documents to address the multi-parent problem #569

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions doorstop/core/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,14 @@ def load(self, reload=False):
self._data[key] = value.strip()
elif key == "digits":
self._data[key] = int(value) # type: ignore
elif key == "mapped_to":
self._data[key] = value.strip()
else:
msg = "unexpected document setting '{}' in: {}".format(
key, self.config
log.debug(
"custom document attribute found: {} = {}".format(key, value)
)
raise DoorstopError(msg)
# custom attribute
self._data[key] = value
except (AttributeError, TypeError, ValueError):
msg = "invalid value for '{}' in: {}".format(key, self.config)
raise DoorstopError(msg)
Expand Down Expand Up @@ -437,6 +440,10 @@ def index(self):
log.info("deleting {} index...".format(self))
common.delete(self.index)

def attribute(self, attrib):
"""Get the item's custom attribute."""
return self._data.get(attrib)

# actions ################################################################

# decorators are applied to methods in the associated classes
Expand Down
10 changes: 9 additions & 1 deletion doorstop/core/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,10 +728,18 @@ def find_child_items_and_documents(self, document=None, tree=None, find_all=True
tree = tree or self.tree
if not document or not tree:
return child_items, child_documents

# get list of mapped documents
mapped_document_prefixes = document.attribute("mapped_to") if document else []
if not mapped_document_prefixes:
mapped_document_prefixes = []

# Find child objects
log.debug("finding item {}'s child objects...".format(self))
for document2 in tree:
if document2.parent == document.prefix:
if (document2.parent == document.prefix) or (
document2.prefix in mapped_document_prefixes
):
child_documents.append(document2)
# Search for child items unless we only need to find one
if not child_items or find_all:
Expand Down
9 changes: 8 additions & 1 deletion doorstop/core/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import logging
import os
from typing import List
from typing import Dict, List
from unittest.mock import MagicMock, Mock, patch

from doorstop.core.base import BaseFileObject
Expand Down Expand Up @@ -95,13 +95,20 @@ def __init__(self):
self.prefix = "RQ"
self._items: List[Item] = []
self.extended_reviewed: List[str] = []
self._data: Dict[str, str] = {}

def __iter__(self):
yield from self._items

def set_items(self, items):
self._items = items

def set_data(self, data):
self._data = data

def attribute(self, name):
return self._data.get(name)


class MockDocumentSkip(MockDocument): # pylint: disable=W0223,R0902
"""Mock Document class that is always skipped in tree placement."""
Expand Down
4 changes: 2 additions & 2 deletions doorstop/core/tests/test_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ def test_load_invalid(self):
def test_load_unknown(self):
"""Verify loading a document config with an unknown key fails."""
self.document._file = YAML_UNKNOWN
msg = "^unexpected document setting 'John' in: .*\\.doorstop.yml$"
self.assertRaisesRegex(DoorstopError, msg, self.document.load)
self.document.load()
self.assertEqual("Doe", self.document.attribute("John"))

def test_load_unknown_attributes(self):
"""Verify loading a document config with unknown attributes fails."""
Expand Down
11 changes: 9 additions & 2 deletions doorstop/core/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,9 +627,16 @@ def _draw_line(self):
def _draw_lines(self, encoding, html_links=False):
"""Generate lines of the tree structure."""
# Build parent prefix string (`getattr` to enable mock testing)
prefix = getattr(self.document, "prefix", "") or str(self.document)
prefix_link = prefix = getattr(self.document, "prefix", "") or str(
self.document
)

attribute_fn = getattr(self.document, "attribute", None)
mapped = attribute_fn("mapped_to") if callable(attribute_fn) else None

prefix += " (" + ",".join(mapped) + ")" if mapped else ""
if html_links:
prefix = '<a href="documents/{0}">{0}</a>'.format(prefix)
prefix = '<a href="documents/{0}">{1}</a>'.format(prefix_link, prefix)
yield prefix
# Build child prefix strings
for count, child in enumerate(self.children, start=1):
Expand Down
67 changes: 62 additions & 5 deletions doorstop/core/validators/item_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,13 @@ def _get_issues_both(self, item, document, tree, skip):

# Verify an item is being linked to (child links)
if settings.CHECK_CHILD_LINKS and item.normative:
find_all = settings.CHECK_CHILD_LINKS_STRICT or False

mapped_document_prefixes = document.attribute("mapped_to")
if not mapped_document_prefixes:
mapped_document_prefixes = []

find_all = settings.CHECK_CHILD_LINKS_STRICT or mapped_document_prefixes

items, documents = item.find_child_items_and_documents(
document=document, tree=tree, find_all=find_all
)
Expand All @@ -209,13 +215,64 @@ def _get_issues_both(self, item, document, tree, skip):
msg = "skipping issues against document %s..."
log.debug(msg, child_document)
continue
msg = "no links from child document: {}".format(child_document)

if child_document.prefix in mapped_document_prefixes:
msg = "no links at all, missing mapped document: {}".format(
child_document
)
else:
msg = "no links at all, missing child document: {}".format(
child_document
)
yield DoorstopWarning(msg)
elif settings.CHECK_CHILD_LINKS_STRICT:

# here items are found but no strict checking is enabled
# only check "mapped_to" as mandatory links
else:
prefix = [item.document.prefix for item in items]
for child in document.children:

found = False
not_found_list = []

# check if at least on of the normal children exist
for child_document in documents:
if child_document.prefix in skip:
msg = "skipping issues against document %s..."
log.debug(msg, child_document)
continue

# handle mapped documents later
if child_document.prefix in mapped_document_prefixes:
continue

if child_document.prefix in prefix:
# found at least one link from child document
found = True
else:
not_found_list.append(child_document.prefix)

# not found anything but not strict: accept a link from any child document
if not found and not settings.CHECK_CHILD_LINKS_STRICT:
for d in not_found_list:
msg = "links found, missing at lest one document: {}".format(d)
yield DoorstopWarning(msg)

if settings.CHECK_CHILD_LINKS_STRICT:
# if strict check: report any document with no child links
for d in not_found_list:
msg = "no links from document: {}".format(d)
yield DoorstopWarning(msg)

# handle mapped documents: they are treated like "strict"
for child in mapped_document_prefixes:
if child in skip:
msg = "skipping issues against mapped document %s..."
log.debug(msg, child)
continue

if child in skip:
continue

if child not in prefix:
msg = "no links from document: {}".format(child)
msg = "no links from mapped document: {}".format(child)
yield DoorstopWarning(msg)