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

Add Items with Custom Default Attributes #403

Merged
merged 3 commits into from
Aug 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/cli/creation.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,31 @@ linked item: TST001 (@/reqs/tests/TST001.yml) -> REQ001 (@/reqs/REQ001.yml)
It is not allowed to create links which would end up in a self reference or
cyclic dependency.

# Add Items with Custom Default Attributes

Items can be added to documents with custom default values for attributes
specified by the command line:

```sh
$ doorstop add -d defaults.yml REQ
building tree...
added item: REQ001 (@/reqs/REQ001.yml)

$ doorstop publish REQ
building tree...
1.0 REQ001

My default text.
```

defaults.yml
```yaml
text: 'My default text.'
```

The command line specified default values override values from the document
configuration.

# Document Configuration

The settings and attribute options of each document are stored in a
Expand Down
2 changes: 1 addition & 1 deletion doorstop/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def run_add(args, cwd, _, catch=True):

# add items to it
for _ in range(args.count):
item = document.add_item(level=args.level)
item = document.add_item(level=args.level, defaults=args.defaults)
utilities.show("added item: {} ({})".format(item.uid, item.relpath))

# Edit item if requested
Expand Down
6 changes: 6 additions & 0 deletions doorstop/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ def _add(subs, shared):
"environment). Useless option without --edit"
),
)
sub.add_argument(
'-d',
'--defaults',
metavar='FILE',
help=("file in YAML format with default values for attributes of the new item"),
)


def _remove(subs, shared):
Expand Down
4 changes: 4 additions & 0 deletions doorstop/cli/tests/files/template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
text: |
Some text
with more than
one line.
2 changes: 1 addition & 1 deletion doorstop/cli/tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ def test_add_no_server(self):
def test_add_custom_server(self, mock_add_item):
"""Verify 'doorstop add' can be called with a custom server."""
self.assertIs(None, main(['add', 'TUT', '--server', '1.2.3.4']))
mock_add_item.assert_called_once_with(level=None)
mock_add_item.assert_called_once_with(defaults=None, level=None)

def test_add_force(self):
"""Verify 'doorstop add' can be called with a missing server."""
Expand Down
26 changes: 26 additions & 0 deletions doorstop/cli/tests/tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,32 @@ def test_clear_links(self):
cp = self.doorstop()
self.assertNotIn(b'suspect link', cp.stderr)

def test_custom_defaults(self):
"""Verify new item with custom defaults is working."""

self.doorstop("create REQ .")

cp = self.doorstop("add -d no/such/file.yml REQ", 1)
self.assertIn(b'ERROR: reading ', cp.stderr)

self.assertFalse(os.path.isfile('REQ001.yml'))

template = os.path.join(FILES, 'template.yml')
self.doorstop("add -d {} REQ".format(template))

self.assertTrue(os.path.isfile('REQ001.yml'))

cp = self.doorstop("publish REQ", stdout=subprocess.PIPE)
self.assertIn(
b'''1.0 REQ001

Some text
with more than
one line.
''',
cp.stdout,
)


if __name__ == '__main__':
logging.basicConfig(format="%(message)s", level=logging.INFO)
Expand Down
9 changes: 6 additions & 3 deletions doorstop/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,12 @@ def read_text(path, encoding='utf-8'):

"""
log.trace("reading text from '{}'...".format(path))
with open(path, 'r', encoding=encoding) as stream:
text = stream.read()
return text
try:
with open(path, 'r', encoding=encoding) as stream:
return stream.read()
except Exception as ex:
msg = "reading '{}' failed: {}".format(path, ex)
raise DoorstopError(msg)


def load_yaml(text, path, loader=yaml.SafeLoader):
Expand Down
30 changes: 20 additions & 10 deletions doorstop/core/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,10 @@ def new(
# Return the document
return document

def load(self, reload=False):
"""Load the document's properties from its file."""
if self._loaded and not reload:
return
log.debug("loading {}...".format(repr(self)))
config = self.config
def _load_with_include(self, yamlfile):
"""Load the YAML file and process input tags."""
# Read text from file
text = self._read(config)
text = self._read(yamlfile)
# Parse YAML data from text
class IncludeLoader(yaml.SafeLoader):
def include(self, node):
Expand All @@ -154,8 +150,15 @@ def include(self, node):
return data

IncludeLoader.add_constructor('!include', IncludeLoader.include)
IncludeLoader.filenames = [config]
data = self._load(text, config, loader=IncludeLoader)
IncludeLoader.filenames = [yamlfile]
return self._load(text, yamlfile, loader=IncludeLoader)

def load(self, reload=False):
"""Load the document's properties from its file."""
if self._loaded and not reload:
return
log.debug("loading {}...".format(repr(self)))
data = self._load_with_include(self.config)
# Store parsed data
sets = data.get('settings', {})
for key, value in sets.items():
Expand Down Expand Up @@ -405,7 +408,7 @@ def index(self):
# actions ################################################################

# decorators are applied to methods in the associated classes
def add_item(self, number=None, level=None, reorder=True):
def add_item(self, number=None, level=None, reorder=True, defaults=None):
"""Create a new item for the document and return it.

:param number: desired item number
Expand All @@ -430,10 +433,17 @@ def add_item(self, number=None, level=None, reorder=True):
else:
next_level = last.level + 1
log.debug("next level: {}".format(next_level))

# Load more defaults before the item is created to avoid partially
# constructed items in case the loading fails.
more_defaults = self._load_with_include(defaults) if defaults else None

uid = UID(self.prefix, self.sep, number, self.digits)
item = Item.new(self.tree, self, self.path, self.root, uid, level=next_level)
if self._attribute_defaults:
item.set_attributes(self._attribute_defaults)
if more_defaults:
item.set_attributes(more_defaults)
if level and reorder:
self.reorder(keep=item)
return item
Expand Down
7 changes: 7 additions & 0 deletions doorstop/core/tests/test_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,13 @@ def test_add_item_with_number(self, mock_new):
None, self.document, FILES, ROOT, 'REQ999', level=Level('2.2')
)

@patch('doorstop.core.item.Item.set_attributes')
def test_add_item_with_defaults(self, mock_set_attributes):
"""Verify an item can be added to a document with defaults."""
self.document._file = "text: 'abc'"
self.document.add_item(defaults='mock.yml')
mock_set_attributes.assert_called_once_with({'text': 'abc'})

@patch('doorstop.core.item.Item.new')
def test_add_item_empty(self, mock_new):
"""Verify an item can be added to an new document."""
Expand Down