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