Skip to content

Commit

Permalink
Handle multiple file input/output, fix some templates vars, format wi…
Browse files Browse the repository at this point in the history
…th black
  • Loading branch information
pawamoy committed Aug 28, 2018
1 parent 6437ff3 commit 2c6672b
Show file tree
Hide file tree
Showing 16 changed files with 526 additions and 319 deletions.
9 changes: 7 additions & 2 deletions src/shellman/__init__.py
Expand Up @@ -11,9 +11,14 @@
write this documentation as text, man, or markdown format on stdout.
"""

__version__ = '0.2.2'
__version__ = "0.2.2"

from .reader import DocFile, DocStream


__all__ = ['DocFile', 'DocStream']
__all__ = ["DocFile", "DocStream"]

# TODO: context injection from command-line / environment variables
# TODO: re-implement --check option with warnings
# TODO: plugin architecture
# TODO: documentation
195 changes: 142 additions & 53 deletions src/shellman/cli.py
Expand Up @@ -20,9 +20,8 @@
import argparse
import os
import sys
from datetime import date

from .reader import DocFile, DocStream
from .reader import DocFile, DocStream, merge
from . import templates
from . import __version__

Expand All @@ -43,41 +42,87 @@ def valid_file(value):
if not value:
raise argparse.ArgumentTypeError("'' is not a valid file path")
elif not os.path.exists(value):
raise argparse.ArgumentTypeError("%s is not a valid file path" %
value)
raise argparse.ArgumentTypeError("%s is not a valid file path" % value)
elif os.path.isdir(value):
raise argparse.ArgumentTypeError(
"%s is a directory, not a regular file" % value)
"%s is a directory, not a regular file" % value
)
return value


def get_parser():
"""Return a parser for the command line arguments."""
parser = argparse.ArgumentParser()

mxg = parser.add_mutually_exclusive_group()

parser.add_argument(
'-0', '-n', '--nice', action='store_true', dest='nice',
help='be nice: return 0 even if warnings (false)')
parser.add_argument(
'-c', '--check', action='store_true', dest='check',
help='check if the documentation is correct (false)')
"-0",
"-n",
"--nice",
action="store_true",
dest="nice",
help="be nice: return 0 even if warnings (default: false)",
)
mxg.add_argument(
"-c",
"--check",
action="store_true",
dest="check",
help="only check if the documentation is correct, no output (default: false)",
)
parser.add_argument(
'-f', '--format', dest='format', default='',
help='template format to choose (different for each template)')
"-f",
"--format",
dest="format",
default="",
help="template format to choose (different for each template)",
)
parser.add_argument(
'-t', '--template', choices=templates.parser_choices(),
default='helptext', dest='template',
help='The Jinja2 template to use. Prefix with "path:" to specify the path '
'to a directory containing a file named "index". '
'Available templates: %s' % ', '.join(templates.names()))
"-t",
"--template",
choices=templates.parser_choices(),
default="helptext",
dest="template",
help='the Jinja2 template to use. Prefix with "path:" to specify the path '
'to a directory containing a file named "index". '
"Available templates: %s" % ", ".join(templates.names()),
)
parser.add_argument(
'-o', '--output', action='store', dest='output',
"-m",
"--merge",
action="store_true",
dest="merge",
help="when multiple files as input, merge their sections in the output (default: false)",
)
mxg.add_argument(
"-o",
"--output",
action="store",
dest="output",
default=None,
help="file to write to (default: stdout)",
)
mxg.add_argument(
"-O",
"--multiple-output",
action="store",
dest="multiple_output",
default=None,
help='file to write to (stdout by default)')
help="output file path formatted for each input file. "
"You can use the following variables: "
"{filename} and {format} (default: not used)",
)
parser.add_argument(
"-w",
"--warn",
action="store_true",
dest="warn",
help="actually display the warnings (default: false)",
)
parser.add_argument(
'-w', '--warn', action='store_true', dest='warn',
help='actually display the warnings (false)')
parser.add_argument('FILE', type=valid_file, nargs='*',
help='path to the file(s) to read')
"FILE", type=valid_file, nargs="*", help="path to the file(s) to read"
)
return parser


Expand All @@ -99,50 +144,94 @@ def main(argv=None):
args = parser.parse_args(argv)

success = True
doc = None
docs = []

if len(args.FILE) > 1 and args.multiple_output and args.merge:
print(
"shellman: error: cannot merge multiple files and output in multiple files.\n"
" Please use --output instead, or remove --merge."
)
return 2

if args.FILE:
for file in args.FILE:
# TODO: actually support multiple files at once.
# Their sections should be merged.
# What about the filename?
doc = DocFile(file)
docs.append(DocFile(file))
# if args.warn:
# doc.warn()
# success &= bool(doc)
else:
print("shellman: reading on standard input -", file=sys.stderr)
try:
doc = DocStream(sys.stdin)
docs.append(DocStream(sys.stdin, name=args.output or ""))
# if args.warn:
# doc.warn()
# success &= bool(doc)
except KeyboardInterrupt:
pass

if doc is None:
return 1

if args.format and not args.format.startswith('.'):
args.format = '.' + args.format
template = templates.templates[args.template].get(args.format)

indent = 4
rendered = template.render(
doc=doc,
context=dict(
indent=indent,
indent_str=indent * " "
),
shellman_version=__version__,
now=date.today()
)

rendered = rendered.rstrip('\n')

if args.output is not None:
with open(args.output, 'w') as write_stream:
print(rendered, file=write_stream)
template = templates.templates[args.template]

if len(docs) == 1:
doc = docs[0]
contents = get_contents(template, args.format, doc)
if args.output:
write(contents, args.output)
elif args.multiple_output:
write(
contents,
args.multiple_output.format(filename=doc.filename, format=args.format),
)
else:
print(contents)
else:
print(rendered)
if args.output or not args.multiple_output:
if args.merge:
doc = merge(
docs, os.path.basename(args.output or common_ancestor(docs))
)
contents = get_contents(template, args.format, doc)
else:
contents = "\n\n\n".join(
get_contents(template, args.format, doc) for doc in docs
)
if args.output:
write(contents, args.output)
else:
print(contents)
elif args.multiple_output:
for doc in docs:
contents = get_contents(template, args.format, doc)
write(
contents,
args.multiple_output.format(
filename=doc.filename, format=args.format
),
)

return 0 if args.nice or success else 1


def get_contents(template, format, doc):
return template.render(format, doc=doc, shellman_version=__version__)


def write(contents, filepath):
with open(filepath, "w") as write_stream:
print(contents, file=write_stream)


def common_ancestor(docs):
splits = [os.path.split(doc.filepath) for doc in docs]
vertical = []
depth = 1
while True:
if not all(len(s) >= depth for s in splits):
break
vertical.append([s[depth - 1] for s in splits])
depth += 1
common = ""
for v in vertical:
if v.count(v[0]) != len(v):
break
common = v[0]
return common or "<VARIOUS_INPUTS>"
60 changes: 34 additions & 26 deletions src/shellman/reader.py
Expand Up @@ -16,19 +16,18 @@

from .tags import TAGS

tag_value_regexp = re.compile(r'^\s*[\\@]([_a-zA-Z][\w-]*)\s+(.+)$')
tag_no_value_regexp = re.compile(r'^\s*[\\@]([_a-zA-Z][\w-]*)\s*$')
tag_value_regexp = re.compile(r"^\s*[\\@]([_a-zA-Z][\w-]*)\s+(.+)$")
tag_no_value_regexp = re.compile(r"^\s*[\\@]([_a-zA-Z][\w-]*)\s*$")


class DocType:
TAG = 'T'
TAG_VALUE = 'TV'
VALUE = 'V'
INVALID = 'I'
TAG = "T"
TAG_VALUE = "TV"
VALUE = "V"
INVALID = "I"


class DocLine:

def __init__(self, path, lineno, tag, value):
self.path = path
self.lineno = lineno
Expand All @@ -44,8 +43,8 @@ def __str__(self):
elif doc_type == DocType.VALUE:
s = '"%s"' % self.value
else:
s = 'invalid'
return '%s:%s: %s: %s' % (self.path, self.lineno, doc_type, s)
s = "invalid"
return "%s:%s: %s: %s" % (self.path, self.lineno, doc_type, s)

def doc_type(self):
if self.tag:
Expand All @@ -67,7 +66,7 @@ def __bool__(self):
return bool(self.lines)

def __str__(self):
return '\n'.join([str(line) for line in self.lines])
return "\n".join([str(line) for line in self.lines])

def append(self, line):
self.lines.append(line)
Expand Down Expand Up @@ -106,11 +105,10 @@ def values(self):


class DocStream:
def __init__(self, stream, name=''):
def __init__(self, stream, name=""):
self.filepath = None
self.filename = name or stream.name
self.sections = process_blocks(
preprocess_lines(preprocess_stream(stream)))
self.sections = process_blocks(preprocess_lines(preprocess_stream(stream)))


class DocFile:
Expand All @@ -120,43 +118,41 @@ def __init__(self, path):
with open(path) as stream:
try:
self.sections = process_blocks(
preprocess_lines(preprocess_stream(stream)))
preprocess_lines(preprocess_stream(stream))
)
except UnicodeDecodeError:
print('Cannot read file %s' % path)
print("Cannot read file %s" % path)
self.sections = []


def preprocess_stream(stream):
for lineno, line in enumerate(stream, 1):
line = line.lstrip(' \t').rstrip('\n')
if line.startswith('##'):
line = line.lstrip(" \t").rstrip("\n")
if line.startswith("##"):
yield stream.name, lineno, line


def preprocess_lines(lines):
current_block = DocBlock()
for path, lineno, line in lines:
line = line.lstrip('#')
line = line.lstrip("#")
res = tag_value_regexp.search(line)
if res:
tag, value = res.groups()
if current_block and not tag.startswith(current_block.tag + '-'):
if current_block and not tag.startswith(current_block.tag + "-"):
yield current_block
current_block = DocBlock()
current_block.append(DocLine(
path, lineno, tag, value))
current_block.append(DocLine(path, lineno, tag, value))
else:
res = tag_no_value_regexp.search(line)
if res:
tag = res.groups()[0]
if current_block and not tag.startswith(current_block.tag + '-'):
if current_block and not tag.startswith(current_block.tag + "-"):
yield current_block
current_block = DocBlock()
current_block.append(DocLine(
path, lineno, tag, ''))
current_block.append(DocLine(path, lineno, tag, ""))
else:
current_block.append(DocLine(
path, lineno, None, line[1:]))
current_block.append(DocLine(path, lineno, None, line[1:]))
if current_block:
yield current_block

Expand All @@ -167,3 +163,15 @@ def process_blocks(blocks):
tag_class = TAGS.get(block.tag, TAGS[None])
sections[block.tag].append(tag_class.from_lines(block.lines))
return dict(sections)


def merge(docs, filename):
stream = object()
stream.name = filename
final_doc = DocStream(stream=stream)
for doc in docs:
for section, values in doc.sections.items():
if section not in final_doc.sections:
final_doc.sections[section] = []
final_doc.sections[section].extend(values)
return final_doc

0 comments on commit 2c6672b

Please sign in to comment.