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

XLSX multisheet #682

Draft
wants to merge 9 commits into
base: develop
Choose a base branch
from
40 changes: 26 additions & 14 deletions doorstop/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,49 +439,61 @@ def run_import(args, cwd, error, catch=True, _tree=None):
document = item = None
attrs = utilities.literal_eval(args.attrs, error)
mapping = utilities.literal_eval(args.map, error)
ext = utilities.get_ext(args, error, None, None)

if args.path:
if not args.prefix:
if not args.prefix and not importer.can_import_tree(ext):
error("when [path] specified, [prefix] is also required")
elif args.document:
error("'--document' cannot be used with [path] [prefix]")
elif args.item:
error("'--item' cannot be used with [path] [prefix]")
ext = utilities.get_ext(args, error, None, None)
elif not (args.document or args.item):
elif not (args.document or args.item or importer.can_import_tree(ext)):
error("specify [path], '--document', or '--item' to import")

request_next_number = _request_next_number(args)
tree = _tree or _get_tree(
args, cwd, request_next_number=request_next_number
)

documents = []
with utilities.capture(catch=catch) as success:
if args.path:
# import supports importing a tree and file passed in with no --document/--item and no prefix
if args.path and os.path.isfile(args.path) and \
importer.can_import_tree(ext) and \
not (args.document or args.item) and \
not args.prefix:
documents = importer.import_file(args.path, ext=ext, mapping=mapping, tree=tree)

# passed a path and prefix
elif args.path and args.prefix:
# get the document
request_next_number = _request_next_number(args)
tree = _tree or _get_tree(
args, cwd, request_next_number=request_next_number
)
document = tree.find_document(args.prefix)
log.debug(f"Found document: '{document}' for prefix {args.prefix}")

# import items into it
msg = "importing '{}' into document {}...".format(args.path, document)
utilities.show(msg, flush=True)
importer.import_file(args.path, document, ext, mapping=mapping)
documents = importer.import_file(args.path, ext=ext, mapping=mapping, document=document)

elif args.document:
prefix, path = args.document
document = importer.create_document(prefix, path, parent=args.parent)
documents = [importer.create_document(prefix, path, parent=args.parent)]
elif args.item:
prefix, uid = args.item
request_next_number = _request_next_number(args)
item = importer.add_item(
prefix, uid, attrs=attrs, request_next_number=request_next_number
)
documents = [item.document]
if not success:
return False

if document:
for doc in documents:
utilities.show(
"imported document: {} ({})".format(document.prefix, document.relpath)
"imported into document: {} ({})".format(doc.prefix, doc.relpath)
)
else:
assert item
if item:
utilities.show("imported item: {} ({})".format(item.uid, item.relpath))

return True
Expand Down
2 changes: 1 addition & 1 deletion doorstop/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def main(args=None): # pylint: disable=R0915
log.debug(f"command cancelled: {args}")
success = False
if success:
log.debug("command succeeded: {args}")
log.debug(f"command succeeded: {args}")
else:
log.debug(f"command failed: {args}")
sys.exit(1)
Expand Down
18 changes: 12 additions & 6 deletions doorstop/cli/tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ class TestImportFile(MockTestCase):

def test_import_file_missing_prefix(self):
"""Verify 'doorstop import' returns an error with a missing prefix."""
path = os.path.join(FILES, "exported.xlsx")
path = os.path.join(FILES, "exported.csv")
self.assertRaises(SystemExit, main, ["import", path])

def test_import_file_extra_flags(self):
Expand Down Expand Up @@ -619,7 +619,7 @@ def test_import_csv_to_document_existing(self):
self.assertIs(None, main(["import", path, "PREFIX"]))
# Assert
path = os.path.join(dirpath, "REQ001.yml")
self.assertTrue(os.path.isfile(path))
self.assertTrue(os.path.isfile(path), f"{path} is not a file")

def test_import_tsv_to_document_existing(self):
"""Verify 'doorstop import' can import TSV to an existing document."""
Expand All @@ -630,7 +630,7 @@ def test_import_tsv_to_document_existing(self):
self.assertIs(None, main(["import", path, "PREFIX"]))
# Assert
path = os.path.join(dirpath, "REQ001.yml")
self.assertTrue(os.path.isfile(path))
self.assertTrue(os.path.isfile(path), f"{path} is not a file")

def test_import_xlsx_to_document_existing(self):
"""Verify 'doorstop import' can import XLSX to an existing document."""
Expand All @@ -641,7 +641,7 @@ def test_import_xlsx_to_document_existing(self):
self.assertIs(None, main(["import", path, "PREFIX"]))
# Assert
path = os.path.join(dirpath, "REQ001.yml")
self.assertTrue(os.path.isfile(path))
self.assertTrue(os.path.isfile(path), f"{path} is not a file")


@unittest.skipUnless(os.getenv(ENV), REASON)
Expand Down Expand Up @@ -702,12 +702,18 @@ def test_export_document_xlsx_error(self):
self.assertRaises(SystemExit, main, ["export", "tut", path])
self.assertFalse(os.path.isfile(path))

def test_export_tree_xlsx(self):
"""Verify 'doorstop export' can create an XLSX directory."""
def test_export_tree_xlsx_dir(self):
"""Verify 'doorstop export' can create an XLSX tree export."""
path = os.path.join(self.temp, "all")
self.assertIs(None, main(["export", "all", path, "--xlsx"]))
self.assertTrue(os.path.isdir(path))

def test_export_tree_xlsx_file(self):
"""Verify 'doorstop export' can create an XLSX tree export."""
path = os.path.join(self.temp, "all.xlsx")
self.assertIs(None, main(["export", "all", path, "--xlsx"]))
self.assertTrue(os.path.isfile(path))

def test_export_tree_no_path(self):
"""Verify 'doorstop export' returns an error with no path."""
self.assertRaises(SystemExit, main, ["export", "all"])
Expand Down
128 changes: 74 additions & 54 deletions doorstop/core/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import yaml

from doorstop import common, settings
from doorstop import common, settings as dsettings
from doorstop.common import (
DoorstopError,
DoorstopInfo,
Expand Down Expand Up @@ -135,7 +135,7 @@ def new(

"""
# Check separator
if sep and sep not in settings.SEP_CHARS:
if sep and sep not in dsettings.SEP_CHARS:
raise DoorstopError("invalid UID separator '{}'".format(sep))

config = os.path.join(path, Document.CONFIG)
Expand Down Expand Up @@ -192,36 +192,63 @@ def include(self, node):
IncludeLoader.filenames = [yamlfile] # type: ignore
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():
try:
if key == "prefix":
@property
def settings(self):
"""
Document settings as a dict
"""
sets = {}
for key, value in self._data.items():
if key == "prefix":
sets[key] = str(value)
elif key == "parent":
if value:
sets[key] = value
else:
sets[key] = value
return sets

@settings.setter
def settings(self, settings):
def fill_key(key, value):
match key:
case "prefix":
self._data[key] = Prefix(value)
elif key == "sep":
case "sep":
self._data[key] = value.strip()
elif key == "parent":
case "parent":
self._data[key] = value.strip()
elif key == "digits":
case "digits":
self._data[key] = int(value) # type: ignore
elif key == "itemformat":
case "itemformat":
self._data[key] = value.strip()
else:
msg = "unexpected document setting '{}' in: {}".format(
key, self.config
)
case _:
msg = f"unexpected document setting '{key}' in: {self.config}"
raise DoorstopError(msg)

for key, value in settings.items():
try:
fill_key(key, value)
except (AttributeError, TypeError, ValueError):
msg = "invalid value for '{}' in: {}".format(key, self.config)
msg = f"invalid value for '{key}' in: {self.config}"
raise DoorstopError(msg)

@property
def attributes(self):
"""
Document attributes as a dict
"""
# Save the attributes
attributes = {}
if self._attribute_defaults:
attributes["defaults"] = self._attribute_defaults
if self._extended_reviewed:
attributes["reviewed"] = self._extended_reviewed
return attributes

@attributes.setter
def attributes(self, attributes):
# Store parsed attributes
attributes = data.get("attributes", {})
for key, value in attributes.items():
if key == "defaults":
self._attribute_defaults = value
Expand All @@ -235,8 +262,15 @@ def load(self, reload=False):
)
raise DoorstopError(msg)

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)
self.settings = data["settings"] if "settings" in data else {}
self.attributes = data["attributes"] if "attributes" in data else {}
self.extensions = data.get("extensions", {})

# Set meta attributes
self._loaded = True
if reload:
Expand All @@ -246,27 +280,13 @@ def load(self, reload=False):
def save(self):
"""Save the document's properties to its file."""
log.debug("saving {}...".format(repr(self)))
# Format the data items

data = {}
sets = {}
for key, value in self._data.items():
if key == "prefix":
sets[key] = str(value)
elif key == "parent":
if value:
sets[key] = value
else:
sets[key] = value
data["settings"] = sets
# Save the attributes
attributes = {}
if self._attribute_defaults:
attributes["defaults"] = self._attribute_defaults
if self._extended_reviewed:
attributes["reviewed"] = self._extended_reviewed
if attributes:
data["attributes"] = attributes
# Dump the data to YAML
if self.settings:
data["settings"] = self.settings,
if self.attributes:
data["attributes"] = self.attributes

text = self._dump(data)
# Save the YAML to file
self._write(text, self.config)
Expand Down Expand Up @@ -313,7 +333,7 @@ def _iter(self, reload=False):
except Exception:
log.error("Unable to load: %s", item)
raise
if settings.CACHE_ITEMS and self.tree:
if dsettings.CACHE_ITEMS and self.tree:
self.tree._item_cache[ # pylint: disable=protected-access
item.uid
] = item
Expand Down Expand Up @@ -392,7 +412,7 @@ def sep(self):
def sep(self, value):
"""Set the prefix-number separator to use for new item UIDs."""
# TODO: raise a specific exception for invalid separator characters?
assert not value or value in settings.SEP_CHARS
assert not value or value in dsettings.SEP_CHARS
self._data["sep"] = value.strip()
# TODO: should the new separator be applied to all items?

Expand Down Expand Up @@ -480,7 +500,7 @@ def index(self, value):
path = os.path.join(self.path, Document.INDEX)
log.info("creating {} index...".format(self))
common.write_lines(
self._lines_index(self.items), path, end=settings.WRITE_LINESEPERATOR
self._lines_index(self.items), path, end=dsettings.WRITE_LINESEPERATOR
)

@index.deleter
Expand Down Expand Up @@ -516,7 +536,7 @@ def add_item(self, number=None, level=None, reorder=True, defaults=None, name=No
name, self.prefix
)
raise DoorstopError(msg)
if self.sep not in settings.SEP_CHARS:
if self.sep not in dsettings.SEP_CHARS:
msg = "cannot add item with name '{}' to document '{}' with an invalid separator '{}'".format(
name, self.prefix, self.sep
)
Expand Down Expand Up @@ -608,13 +628,13 @@ def reorder(self, manual=True, automatic=True, start=None, keep=None, _items=Non
@staticmethod
def _lines_index(items):
"""Generate (pseudo) YAML lines for the document index."""
yield "#" * settings.MAX_LINE_LENGTH
yield "#" * dsettings.MAX_LINE_LENGTH
yield "# THIS TEMPORARY FILE WILL BE DELETED AFTER DOCUMENT REORDERING"
yield "# MANUALLY INDENT, DEDENT, & MOVE ITEMS TO THEIR DESIRED LEVEL"
yield "# A NEW ITEM WILL BE ADDED FOR ANY UNKNOWN IDS, i.e. - new: "
yield "# THE COMMENT WILL BE USED AS THE ITEM TEXT FOR NEW ITEMS"
yield "# CHANGES WILL BE REFLECTED IN THE ITEM FILES AFTER CONFIRMATION"
yield "#" * settings.MAX_LINE_LENGTH
yield "#" * dsettings.MAX_LINE_LENGTH
yield ""
yield "initial: {}".format(items[0].level if items else 1.0)
yield "outline:"
Expand All @@ -623,8 +643,8 @@ def _lines_index(items):
lines = item.text.strip().splitlines()
comment = lines[0].replace("\\", "\\\\") if lines else ""
line = space + "- {u}: # {c}".format(u=item.uid, c=comment)
if len(line) > settings.MAX_LINE_LENGTH:
line = line[: settings.MAX_LINE_LENGTH - 3] + "..."
if len(line) > dsettings.MAX_LINE_LENGTH:
line = line[: dsettings.MAX_LINE_LENGTH - 3] + "..."
yield line

@staticmethod
Expand Down Expand Up @@ -854,9 +874,9 @@ def get_issues(
return

# Reorder or check item levels
if settings.REORDER:
if dsettings.REORDER:
self.reorder(_items=items)
elif settings.CHECK_LEVELS:
elif dsettings.CHECK_LEVELS:
yield from self._get_issues_level(items)

item_validator = ItemValidator()
Expand Down
Loading
Loading