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 @@
self._data[key] = value.strip()
elif key == "digits":
self._data[key] = int(value) # type: ignore
elif key == "mapped_to":
self._data[key] = value.strip()

Check warning on line 190 in doorstop/core/document.py

View check run for this annotation

Codecov / codecov/patch

doorstop/core/document.py#L190

Added line #L190 was not covered by tests
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 @@
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_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)

Check warning on line 639 in doorstop/core/tree.py

View check run for this annotation

Codecov / codecov/patch

doorstop/core/tree.py#L639

Added line #L639 was not covered by tests
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 @@

# 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 @@
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(

Check warning on line 220 in doorstop/core/validators/item_validator.py

View check run for this annotation

Codecov / codecov/patch

doorstop/core/validators/item_validator.py#L220

Added line #L220 was not covered by tests
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

Check warning on line 242 in doorstop/core/validators/item_validator.py

View check run for this annotation

Codecov / codecov/patch

doorstop/core/validators/item_validator.py#L240-L242

Added lines #L240 - L242 were not covered by tests

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

Check warning on line 246 in doorstop/core/validators/item_validator.py

View check run for this annotation

Codecov / codecov/patch

doorstop/core/validators/item_validator.py#L246

Added line #L246 was not covered by tests

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

Check warning on line 271 in doorstop/core/validators/item_validator.py

View check run for this annotation

Codecov / codecov/patch

doorstop/core/validators/item_validator.py#L269-L271

Added lines #L269 - L271 were not covered by tests

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)

Check warning on line 277 in doorstop/core/validators/item_validator.py

View check run for this annotation

Codecov / codecov/patch

doorstop/core/validators/item_validator.py#L277

Added line #L277 was not covered by tests
yield DoorstopWarning(msg)