Doorstop: Requirements Management Using Python and Version Control

Installation

In [ ]:
!pip uninstall doorstop --yes
!pip install doorstop==0.8.1
In [1]:
import doorstop

Functions

In [2]:
tree = doorstop.build()

print(tree)
SYS <- [ HLR <- [ HLT, LLR <- [ LLT ] ] ]
In [3]:
document = doorstop.find_document('sys')

print(document)
SYS
In [4]:
item = doorstop.find_item('sys1')

print(item)
SYS001

Exceptions

In [5]:
doorstop.find_item('fake99')  # raises exception
---------------------------------------------------------------------------
DoorstopError                             Traceback (most recent call last)
<ipython-input-5-2395171557ca> in <module>()
----> 1 doorstop.find_item('fake99')  # raises exception

/Users/Browning/Drive/Profession/BarCamp/Doorstop/doorstop/core/builder.py in find_item(uid)
     84     """Find an item without an explicitly building a tree."""
     85     tree = _get_tree()
---> 86     item = tree.find_item(uid)
     87     return item
     88 

/Users/Browning/Drive/Profession/BarCamp/Doorstop/doorstop/core/tree.py in find_item(self, value, _kind)
    412                 log.trace("cached unknown: {}".format(uid))
    413 
--> 414         raise DoorstopError(UID.UNKNOWN_MESSAGE.format(k=_kind, u=uid))
    415 
    416     def get_issues(self, document_hook=None, item_hook=None):

DoorstopError: no item with UID: fake99

Classes

Items

In [6]:
item = doorstop.find_item('hlr003')

print(repr(item))
Item('/Users/Browning/Drive/Programs/Desktop/DoorstopDemo/reqs/hlr/HLR003.yml')
In [7]:
with open(item.path) as infile:
    print(infile.read())
active: true
derived: false
level: 1.2
links:
- SYS002: d880b59c0bc5a060d41922110b833f3f
- SYS008: 6a2015090c976f3aebaa431a7fdeda60
- SYS028: 5530ac30d996ebc2841d663698f0756a
normative: true
ref: test_tutorial.py
reviewed: 6fc06b5807481efef80ab361d1f196f1
text: |
  Foo SHALL bar.

Properties

In [8]:
print("path:", item.path)
print("relpath:", item.relpath)
path: /Users/Browning/Drive/Programs/Desktop/DoorstopDemo/reqs/hlr/HLR003.yml
relpath: @/reqs/hlr/HLR003.yml
In [9]:
print("id:", item.uid)
print("prefix:", item.prefix)
print("number:", item.number)
id: HLR003
prefix: HLR
number: 3
In [10]:
item.level = "1.2"

print("level:", item.level)
level: 1.2
In [11]:
item.active = True
item.derived = False

print("active:", item.active)
print("derived:", item.derived)
print("normative:", item.normative)
print("heading:", item.heading)
active: True
derived: False
normative: True
heading: False
In [12]:
item.text = "Foo SHALL bar."

print("text:", item.text)
text: Foo SHALL bar.
In [13]:
item.ref = "test_tutorial.py"

print("ref:", item.ref)
ref: test_tutorial.py
In [14]:
for identifier in item.links:
    print("id:", identifier)
id: SYS002
id: SYS008
id: SYS028

Actions

In [15]:
item.link('SYS999')

for identifier in item.links:
    print("id:", identifier)
id: SYS002
id: SYS008
id: SYS028
id: SYS999
In [16]:
item.unlink('SYS999')

for identifier in item.links:
    print("id:", identifier)
id: SYS002
id: SYS008
id: SYS028
In [17]:
item.ref = "Represents an item file with linkable text."

path, line = item.find_ref()

print("path:", path)
print("line:", line)
path: demo/core/item.py
line: 16
In [18]:
item.ref = "test_tutorial.py"

path, line = item.find_ref()

print("path:", path)
print("line:", line)
path: demo/cli/test/test_tutorial.py
line: None
In [19]:
document = tree.find_document(item.prefix)

identifiers = item.find_child_links()  # reverse links

for identifier in identifiers:
    print("id:", identifier)
id: LLR007
id: LLR031
id: LLR036
id: HLT031
id: HLT172
id: HLT142
id: HLT196
id: HLT107
id: HLT057
id: HLT103
id: LLR119
id: LLR156
In [20]:
item.text = "Another change."
item.reviewed
Out[20]:
False
In [21]:
item.review()
item.reviewed
Out[21]:
True
In [22]:
item.link('SYS042')
item.cleared
Out[22]:
False
In [23]:
item.clear()
item.cleared
Out[23]:
True

Documents

In [24]:
document = doorstop.find_document('hlt')

print(repr(document))
Document('/Users/Browning/Drive/Programs/Desktop/DoorstopDemo/demo/cli/test/docs')
In [25]:
with open(document.config) as infile:
    print(infile.read())
settings:
  digits: 3
  parent: HLR
  prefix: HLT
  sep: ''

Properties

In [26]:
print("path:", document.path)
print("relpath:", document.relpath)
path: /Users/Browning/Drive/Programs/Desktop/DoorstopDemo/demo/cli/test/docs
relpath: @/demo/cli/test/docs
In [27]:
print("prefix:", document.prefix)
print("sep:", document.sep)
print("digits:", document.digits)
prefix: HLT
sep: 
digits: 3
In [28]:
print("parent:", document.parent)
parent: HLR
In [29]:
count = 0

for item in document:
    print(item)
    
    count += 1
    if count > 10:
        print('...')
        break
HLT001
HLT002
HLT003
HLT004
HLT005
HLT006
HLT007
HLT008
HLT009
HLT010
HLT011
...

Actions

In [30]:
item = document.add_item()

print(item)
HLT201
In [31]:
document.find_item(item.uid)
Out[31]:
Item('/Users/Browning/Drive/Programs/Desktop/DoorstopDemo/demo/cli/test/docs/HLT201.yml')
In [32]:
document.remove_item(item.uid)
Out[32]:
Item('/Users/Browning/Drive/Programs/Desktop/DoorstopDemo/demo/cli/test/docs/HLT201.yml')
In [33]:
document.find_item(item.uid)  # raises exception
---------------------------------------------------------------------------
DoorstopError                             Traceback (most recent call last)
<ipython-input-33-3d1767e1ed93> in <module>()
----> 1 document.find_item(item.uid)  # raises exception

/Users/Browning/Drive/Profession/BarCamp/Doorstop/doorstop/core/document.py in find_item(self, value, _kind)
    576                 return item
    577 
--> 578         raise DoorstopError("no matching{} UID: {}".format(_kind, uid))
    579 
    580     def get_issues(self, item_hook=None, **kwargs):

DoorstopError: no matching UID: HLT201

Trees

In [34]:
tree = doorstop.build()

print(repr(tree))
<Tree SYS <- [ HLR <- [ HLT, LLR <- [ LLT ] ] ]>

Properites

In [35]:
print("vcs:", tree.vcs)
vcs: <doorstop.core.vcs.git.WorkingCopy object at 0x107f0e400>

Actions

In [36]:
import os

path = os.path.join(tree.root, 'a', 'temporaty', 'directory')
document = tree.create_document(path, 'TMP', parent='SYS')

print(document.relpath)

document.delete()
@/a/temporaty/directory
Out[36]:
Document('/Users/Browning/Drive/Programs/Desktop/DoorstopDemo/a/temporaty/directory')
In [37]:
item = tree.add_item('sys')

print(item.relpath)
@/reqs/sys/SYS051.yml
In [38]:
tree.remove_item(item.uid)
Out[38]:
Item('/Users/Browning/Drive/Programs/Desktop/DoorstopDemo/reqs/sys/SYS051.yml')
In [39]:
tree.link_items('llt42', 'hlr2')
Out[39]:
(Item('/Users/Browning/Drive/Programs/Desktop/DoorstopDemo/demo/core/test/docs/LLT042.yml'),
 Item('/Users/Browning/Drive/Programs/Desktop/DoorstopDemo/reqs/hlr/HLR002.yml'))
In [40]:
tree.unlink_items('llt42', 'hlr2')
Out[40]:
(Item('/Users/Browning/Drive/Programs/Desktop/DoorstopDemo/demo/core/test/docs/LLT042.yml'),
 Item('/Users/Browning/Drive/Programs/Desktop/DoorstopDemo/reqs/hlr/HLR002.yml'))

Validation

In [41]:
tree = doorstop.build()

for issue in tree.issues:
    print(issue)
HLR: HLR003: unreviewed changes
HLT: HLT031: suspect link: HLR003
HLT: HLT172: suspect link: HLR003
HLT: HLT142: suspect link: HLR003
HLT: HLT196: suspect link: HLR003
HLT: HLT107: suspect link: HLR003
HLT: HLT057: suspect link: HLR003
HLT: HLT103: suspect link: HLR003
LLR: LLR007: suspect link: HLR003
LLR: LLR031: suspect link: HLR003
LLR: LLR036: suspect link: HLR003
LLR: LLR119: suspect link: HLR003
LLR: LLR156: suspect link: HLR003
In [42]:
item = tree.find_item('hlr2')
item.links = []

item = tree.find_item('llr2')
item.link('fake99')

for issue in tree.issues:
    print(issue)
HLR: HLR002: no links to parent document: SYS
HLR: HLR002: unreviewed changes
HLR: HLR003: unreviewed changes
HLT: HLT031: suspect link: HLR003
HLT: HLT172: suspect link: HLR003
HLT: HLT142: suspect link: HLR003
HLT: HLT196: suspect link: HLR003
HLT: HLT107: suspect link: HLR003
HLT: HLT057: suspect link: HLR003
HLT: HLT103: suspect link: HLR003
LLR: LLR002: parent is 'HLR', but linked to: fake99
LLR: LLR002: linked to unknown item: fake99
LLR: LLR002: unreviewed changes
LLR: LLR007: suspect link: HLR003
LLR: LLR031: suspect link: HLR003
LLR: LLR036: suspect link: HLR003
LLR: LLR119: suspect link: HLR003
LLR: LLR156: suspect link: HLR003
In [43]:
from doorstop.common import DoorstopWarning, DoorstopInfo

def document_hook(document, tree):
    if len(document.items) <= 50:
        yield DoorstopInfo("50 or fewer items")

def item_hook(item, document, tree):
    if 'mater tales' in item.text:
        yield DoorstopWarning("'mater tales' in text")

for issue in tree.get_issues(document_hook=document_hook, item_hook=item_hook):
    print(issue)
SYS: 50 or fewer items
HLR: HLR002: no links to parent document: SYS
HLR: HLR002: unreviewed changes
HLR: HLR003: unreviewed changes
HLR: HLR023: 'mater tales' in text
HLT: HLT031: suspect link: HLR003
HLT: HLT172: suspect link: HLR003
HLT: HLT142: suspect link: HLR003
HLT: HLT196: suspect link: HLR003
HLT: HLT107: suspect link: HLR003
HLT: HLT057: suspect link: HLR003
HLT: HLT103: suspect link: HLR003
LLR: LLR002: parent is 'HLR', but linked to: fake99
LLR: LLR002: linked to unknown item: fake99
LLR: LLR002: unreviewed changes
LLR: LLR007: suspect link: HLR003
LLR: LLR031: suspect link: HLR003
LLR: LLR036: suspect link: HLR003
LLR: LLR119: suspect link: HLR003
LLR: LLR156: suspect link: HLR003