Passed
Pull Request — develop (#458)
by
unknown
02:49
created

doorstop.core.publisher.publish()   C

Complexity

Conditions 9

Size

Total Lines 53
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 22
nop 6
dl 0
loc 53
rs 6.6666
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
"""Functions to publish documents and items."""
2
3
import os
4
import textwrap
5
6
import markdown
7
8
from doorstop import common
9
from doorstop.common import DoorstopError
10
from doorstop.core.types import iter_documents, iter_items, is_tree, is_item
11
from doorstop import settings
12
13
CSS = os.path.join(os.path.dirname(__file__), 'files', 'doorstop.css')
14
INDEX = 'index.html'
15
16
log = common.logger(__name__)
17
18
19
def publish(obj, path, ext=None, linkify=None, index=None, **kwargs):
20
    """Publish an object to a given format.
21
22
    The function can be called in two ways:
23
24
    1. document or item-like object + output file path
25
    2. tree-like object + output directory path
26
27
    :param obj: (1) Item, list of Items, Document or (2) Tree
28
    :param path: (1) output file path or (2) output directory path
29
    :param ext: file extension to override output extension
30
    :param linkify: turn links into hyperlinks (for Markdown or HTML)
31
    :param index: create an index.html (for HTML)
32
33
    :raises: :class:`doorstop.common.DoorstopError` for unknown file formats
34
35
    :return: output location if files created, else None
36
37
    """
38
    # Determine the output format
39
    ext = ext or os.path.splitext(path)[-1] or '.html'
40
    check(ext)
41
    if linkify is None:
42
        linkify = is_tree(obj) and ext == '.html'
43
    if index is None:
44
        index = is_tree(obj) and ext == '.html'
45
46
    # Publish documents
47
    count = 0
48
    for obj2, path2 in iter_documents(obj, path, ext):
49
        count += 1
50
51
        # Publish content to the specified path
52
        common.create_dirname(path2)
53
        log.info("publishing to {}...".format(path2))
54
        # publish_lines returnt geneartor objext mit yields fuer jede zeile des source-codes darin
55
        lines = publish_lines(obj2, ext, linkify=linkify, **kwargs)
56
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
57
        # schreibt zeilen indem über elemente des generator iteriert wird und dise in ein file unter path2 geschrieben werden
58
        common.write_lines(lines, path2)
59
60
    # Create index
61
    if index and count:
62
        _index(path, tree=obj if is_tree(obj) else None)
63
64
    # Return the published path
65
    if count:
66
        msg = "published to {} file{}".format(count, 's' if count > 1 else '')
67
        log.info(msg)
68
        return path
69
    else:
70
        log.warning("nothing to publish")
71
        return None
72
73
74
def _index(directory, index=INDEX, extensions=('.html',), tree=None):
75
    """Create an HTML index of all files in a directory.
76
77
    :param directory: directory for index
78
    :param index: filename for index
79
    :param extensions: file extensions to include
80
    :param tree: optional tree to determine index structure
81
82
    """
83
    # Get paths for the index index
84
    filenames = []
85
    for filename in os.listdir(directory):
86
        if filename.endswith(extensions) and filename != INDEX:
87
            filenames.append(os.path.join(filename))
88
89
    # Create the index
90
    if filenames:
91
        path = os.path.join(directory, index)
92
        log.info("creating an {}...".format(index))
93
        lines = _lines_index(filenames, tree=tree)
94
        common.write_lines(lines, path)
95
    else:
96
        log.warning("no files for {}".format(index))
97
98
99
def _lines_index(filenames, charset='UTF-8', tree=None):
100
    """Yield lines of HTML for index.html.
101
102
    :param filesnames: list of filenames to add to the index
103
    :param charset: character encoding for output
104
    :param tree: optional tree to determine index structure
105
106
    """
107
    yield '<!DOCTYPE html>'
108
    yield '<head>'
109
    yield ('<meta http-equiv="content-type" content="text/html; '
110
           'charset={charset}">'.format(charset=charset))
111
    yield '<style type="text/css">'
112
    yield from _lines_css()
113
    yield '</style>'
114
    yield '</head>'
115
    yield '<body>'
116
    # Tree structure
117
    text = tree.draw() if tree else None
118
    if text:
119
        yield ''
120
        yield '<h3>Tree Structure:</h3>'
121
        yield '<pre><code>' + text + '</pre></code>'
122
    # Additional files
123
    if filenames:
124
        if text:
125
            yield ''
126
            yield '<hr>'
127
        yield ''
128
        yield '<h3>Published Documents:</h3>'
129
        yield '<p>'
130
        yield '<ul>'
131
        for filename in filenames:
132
            name = os.path.splitext(filename)[0]
133
            yield '<li> <a href="{f}">{n}</a> </li>'.format(f=filename, n=name)
134
        yield '</ul>'
135
        yield '</p>'
136
    # Traceability table
137
    documents = tree.documents if tree else None
138
    if documents:
139
        if text or filenames:
140
            yield ''
141
            yield '<hr>'
142
        yield ''
143
        # table
144
        yield '<h3>Item Traceability:</h3>'
145
        yield '<p>'
146
        yield '<table>'
147
        # header
148
        for document in documents:
0 ignored issues
show
introduced by
Non-iterable value documents is used in an iterating context
Loading history...
149
            yield '<col width="100">'
150
        yield '<tr>'
151
        for document in documents:
0 ignored issues
show
introduced by
Non-iterable value documents is used in an iterating context
Loading history...
152
            link = '<a href="{p}.html">{p}</a>'.format(p=document.prefix)
153
            yield '  <th height="25" align="center"> {l} </th>'.format(l=link)
154
        yield '</tr>'
155
        # data
156
        for index, row in enumerate(tree.get_traceability()):
157
            if index % 2:
158
                yield '<tr class="alt">'
159
            else:
160
                yield '<tr>'
161
            for item in row:
162
                if item is None:
163
                    link = ''
164
                else:
165
                    link = _format_html_item_link(item)
166
                yield '  <td height="25" align="center"> {} </td>'.format(link)
167
            yield '</tr>'
168
        yield '</table>'
169
        yield '</p>'
170
    yield ''
171
    yield '</body>'
172
    yield '</html>'
173
174
175
def _lines_css():
176
    """Yield lines of CSS to embedded in HTML."""
177
    yield ''
178
    for line in common.read_lines(CSS):
179
        yield line.rstrip()
180
    yield ''
181
182
183
def publish_lines(obj, ext='.txt', **kwargs):
184
    """Yield lines for a report in the specified format.
185
186
    :param obj: Item, list of Items, or Document to publish
187
    :param ext: file extension to specify the output format
188
189
    :raises: :class:`doorstop.common.DoorstopError` for unknown file formats
190
191
    """
192
    # gen returnt eine function des types _lines_html wenn die extension '.html' ist 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
193
    gen = check(ext)
194
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
195
    log.debug("yielding {} as lines of {}...".format(obj, ext))
196
    yield from gen(obj, **kwargs)
197
198
199
def _lines_text(obj, indent=8, width=79, **_):
200
    """Yield lines for a text report.
201
202
    :param obj: Item, list of Items, or Document to publish
203
    :param indent: number of spaces to indent text
204
    :param width: maximum line length
205
206
    :return: iterator of lines of text
207
208
    """
209
    for item in iter_items(obj):
210
211
        level = _format_level(item.level)
212
213
        if item.heading:
214
215
            # Level and Text
216
            yield "{l:<{s}}{t}".format(l=level, s=indent, t=item.text)
217
218
        else:
219
220
            # Level and UID
221
            yield "{l:<{s}}{u}".format(l=level, s=indent, u=item.uid)
222
223
            # Text
224
            if item.text:
225
                yield ""  # break before text
226
                for line in item.text.splitlines():
227
                    yield from _chunks(line, width, indent)
228
229
                    if not line:  # pragma: no cover (integration test)
230
                        yield ""  # break between paragraphs
231
232
            # Reference
233
            if item.ref:
234
                yield ""  # break before reference
235
                ref = _format_text_ref(item)
236
                yield from _chunks(ref, width, indent)
237
238
            # Links
239
            if item.links:
240
                yield ""  # break before links
241
                if settings.PUBLISH_CHILD_LINKS:
242
                    label = "Parent links: "
243
                else:
244
                    label = "Links: "
245
                slinks = label + ', '.join(str(l) for l in item.links)
246
                yield from _chunks(slinks, width, indent)
247
            if settings.PUBLISH_CHILD_LINKS:
248
                links = item.find_child_links()
249
                if links:
250
                    yield ""  # break before links
251
                    slinks = "Child links: " + ', '.join(str(l) for l in links)
252
                    yield from _chunks(slinks, width, indent)
253
254
        yield ""  # break between items
255
256
257
def _chunks(text, width, indent):
258
    """Yield wrapped lines of text."""
259
    yield from textwrap.wrap(text, width,
260
                             initial_indent=' ' * indent,
261
                             subsequent_indent=' ' * indent)
262
263
# Old function can be seen commented below
264
#add 09.01.2020-14.01.2020
265
def _lines_markdown(obj, linkify=False):
0 ignored issues
show
Unused Code introduced by
The argument linkify seems to be unused.
Loading history...
266
    """ Yield lines for ILS html output of requirements.
267
        This function gets changed completly due to confirm ILS stamdarts
268
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
269
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
270
     :param obj: Item, list of Items, or Document to publish
271
     :param linkify: turn links into hyperlinks (for conversion to HTML)
272
     
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
273
     return iterator fo lines of text"""
274
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
275
    for item in iter_items(obj):
276
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
277
278
        if item.Is_Req:
279
            # create header line 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
280
            yield '<table width=100% >'
281
            yield '<tbody>'
282
            yield '<tr>'
283
            yield '<th colspan="3" width=30% style="background=:#eaeaea">'
284
            yield '<div class="Center-align">'
285
            yield  str(item.uid) +'-' + str(item.SPEC_VERSION)
286
            yield '</div>'
287
            yield '</th>'
288
            yield '<th colspan="3" width=50% style="background=:#eaeaea">'
289
            yield '<div class="Center-align">'
290
            yield  str(item.SPEC_SHORT_DECRIPTION)
291
            yield '</div>'
292
            yield '</th>'
293
            yield '<th colspan="3" width=50% style="background=:#eaeaea">'
294
            yield '<div class="Center-align">'
295
            yield  str(item.SPEC_STATUS)
296
            yield '</div>'
297
            yield '</th>'
298
            yield '</tr>'
299
300
            # create tmp dict with content
301
            tmp_dict = {'Requirement': item.text, 'Rationale': item.SPEC_RATIONALE, 'Assumption': item.Assumption,'Additional': item.Add_Info}
0 ignored issues
show
Coding Style introduced by
Exactly one space required after comma
Loading history...
302
            # loop over different contents to fill table 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
303
            for key, value in tmp_dict.items():
304
            
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
305
                yield '<tr>'
306
307
                yield '<th colspan="2" width=20% style="background=:#eaeaea">'
308
                yield '<div class="Center-align">'
309
                yield key 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
310
                yield '</div>'
311
                yield '</th>'
312
313
                yield '<td colspan="8" width=80% style="background=:#eaeaea">'
314
                yield '<div class="Center-align">'
315
                yield ""
316
                # tmp_text kann für zeilenumbrüche im html verwendet werden
317
                #tmp_text = "<br />".join(value.split("\n"))
318
                yield  str(value) 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
319
                yield '</div>'
320
                yield '</td>'
321
322
                yield '</tr>'
323
324
    
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
325
326
327
            yield '<tbody>'
328
            yield '</table>'
329
            
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
330
            
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
331
332
        elif item.EXTENSION:
333
            # create header line 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
334
            yield '<table width=100% >'
335
            yield '<tbody>'
336
            yield '<tr>'
337
            yield '<th colspan="3" width=30% style="background=:#eaeaea">'
338
            yield '<div class="Center-align">'
339
            yield  str(item.uid) +'-'+item.EXTENSION + '-'
340
            yield '</div>'
341
            yield '</th>'
342
            yield '<th colspan="6" width=70% style="background=:#eaeaea">'
343
            yield '<div class="Center-align">'
344
            yield  str(item.TITLE)
345
            yield '</div>'
346
            yield '</tr>'
347
348
349
            
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
350
            yield '<tr>'
351
352
            yield '<th colspan="2" width=20% style="background=:#eaeaea">'
353
            yield '<div class="Center-align">'
354
            yield item.SIDEBAR 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
355
            yield '</div>'
356
            yield '</th>'
357
358
            yield '<td colspan="8" width=80% style="background=:#eaeaea">'
359
            yield '<div class="Center-align">'
360
            yield ""
361
            #tmp_text kann für zeilenumbrüche im HTML genommen weredn
362
            #tmp_text = "<br />".join(item.text.split("\n"))
363
            yield item.text
364
            yield '</div>'
365
            yield '</td>'
366
367
            yield '</tr>'
368
369
            yield '<tbody>'
370
            yield '</table>'
371
372
        else:
373
            text =  item.text
0 ignored issues
show
Coding Style introduced by
Exactly one space required after assignment
Loading history...
374
            yield ""
375
            yield text
376
            yield ""
377
378
    # """Yield lines for a Markdown report.
379
380
    # :param obj: Item, list of Items, or Document to publish
381
    # :param linkify: turn links into hyperlinks (for conversion to HTML)
382
383
    # :return: iterator of lines of text
384
385
    # """
386
    # for item in iter_items(obj):
387
388
    #     heading = '#' * item.depth
389
    #     level = _format_level(item.level)
390
391
    #     if item.heading:
392
393
    #         # Level and Text
394
    #         standard = "{h} {l} {t}".format(h=heading, l=level, t=item.text)
395
    #         attr_list = _format_md_attr_list(item, linkify)
396
    #         yield standard + attr_list
397
398
    #     else:
399
400
    #         # Level and UID
401
    #         if settings.PUBLISH_BODY_LEVELS:
402
    #             standard = "{h} {l} {u}".format(h=heading, l=level, u=item.uid)
403
    #         else:
404
    #             standard = "{h} {u}".format(h=heading, u=item.uid)
405
    #         attr_list = _format_md_attr_list(item, linkify)
406
    #         yield standard + attr_list
407
408
    #         # Text
409
    #         if item.text:
410
    #             yield ""  # break before text
411
    #             yield from item.text.splitlines()
412
413
    #         # Reference
414
    #         if item.ref:
415
    #             yield ""  # break before reference
416
    #             yield _format_md_ref(item)
417
418
    #         # Parent links
419
    #         if item.links:
420
    #             yield ""  # break before links
421
    #             items2 = item.parent_items
422
    #             if settings.PUBLISH_CHILD_LINKS:
423
    #                 label = "Parent links:"
424
    #             else:
425
    #                 label = "Links:"
426
    #             links = _format_md_links(items2, linkify)
427
    #             label_links = _format_md_label_links(label, links, linkify)
428
    #             yield label_links
429
430
    #         # Child links
431
    #         if settings.PUBLISH_CHILD_LINKS:
432
    #             items2 = item.find_child_items()
433
    #             if items2:
434
    #                 yield ""  # break before links
435
    #                 label = "Child links:"
436
    #                 links = _format_md_links(items2, linkify)
437
    #                 label_links = _format_md_label_links(label, links, linkify)
438
    #                 yield label_links
439
440
    #     yield ""  # break between items
441
442
443
def _format_level(level):
444
    """Convert a level to a string and keep zeros if not a top level."""
445
    text = str(level)
446
    if text.endswith('.0') and len(text) > 3:
447
        text = text[:-2]
448
    return text
449
450
451
def _format_md_attr_list(item, linkify):
452
    """Create a Markdown attribute list for a heading."""
453
    return " {{: #{u} }}".format(u=item.uid) if linkify else ''
454
455
456
def _format_text_ref(item):
457
    """Format an external reference in text."""
458
    if settings.CHECK_REF:
459
        path, line = item.find_ref()
460
        path = path.replace('\\', '/')  # always use unix-style paths
461
        if line:
462
            return "Reference: {p} (line {l})".format(p=path, l=line)
463
        else:
464
            return "Reference: {p}".format(p=path)
465
    else:
466
        return "Reference: '{r}'".format(r=item.ref)
467
468
469
def _format_md_ref(item):
470
    """Format an external reference in Markdown."""
471
    if settings.CHECK_REF:
472
        path, line = item.find_ref()
473
        path = path.replace('\\', '/')  # always use unix-style paths
474
        if line:
475
            return "> `{p}` (line {l})".format(p=path, l=line)
476
        else:
477
            return "> `{p}`".format(p=path)
478
    else:
479
        return "> '{r}'".format(r=item.ref)
480
481
482
def _format_md_links(items, linkify):
483
    """Format a list of linked items in Markdown."""
484
    links = []
485
    for item in items:
486
        link = _format_md_item_link(item, linkify=linkify)
487
        links.append(link)
488
    return ', '.join(links)
489
490
491
def _format_md_item_link(item, linkify=True):
492
    """Format an item link in Markdown."""
493
    if linkify and is_item(item):
494
        return "[{u}]({p}.html#{u})".format(u=item.uid, p=item.document.prefix)
495
    else:
496
        return str(item.uid)  # if not `Item`, assume this is an `UnknownItem`
497
498
499
def _format_html_item_link(item, linkify=True):
500
    """Format an item link in HTML."""
501
    if linkify and is_item(item):
502
        link = '<a href="{p}.html#{u}">{u}</a>'.format(u=item.uid,
503
                                                       p=item.document.prefix)
504
        return link
505
    else:
506
        return str(item.uid)  # if not `Item`, assume this is an `UnknownItem`
507
508
509
def _format_md_label_links(label, links, linkify):
510
    """Join a string of label and links with formatting."""
511
    if linkify:
512
        return "*{lb}* {ls}".format(lb=label, ls=links)
513
    else:
514
        return "*{lb} {ls}*".format(lb=label, ls=links)
515
516
517
def _lines_html(obj, linkify=False, charset='UTF-8'):
518
    """Yield lines for an HTML report.
519
520
    :param obj: Item, list of Items, or Document to publish
521
    :param linkify: turn links into hyperlinks
522
523
    :return: iterator of lines of text
524
525
    """
526
    # Determine if a full HTML document should be generated
527
    try:
528
        iter(obj)
529
    except TypeError:
530
        document = False
531
    else:
532
        document = True
533
    # Generate HTML
534
    if document:
535
        yield '<!DOCTYPE html>'
536
        yield '<head>'
537
        yield ('<meta http-equiv="content-type" content="text/html; '
538
               'charset={charset}">'.format(charset=charset))
539
        yield '<style type="text/css">'
540
        yield from _lines_css()
541
        yield '</style>'
542
        yield '</head>'
543
        yield '<body>'
544
    text = '\n'.join(_lines_markdown(obj, linkify=linkify))
545
    html = markdown.markdown(text, extensions=['extra', 'nl2br', 'sane_lists'])
546
    yield from html.splitlines()
547
    if document:
548
        yield '</body>'
549
        yield '</html>'
550
551
552
# Mapping from file extension to lines generator
553
FORMAT_LINES = {'.txt': _lines_text,
554
                '.md': _lines_markdown,
555
                '.html': _lines_html}
556
557
558
def check(ext):
559
    """Confirm an extension is supported for publish.
560
561
    :raises: :class:`doorstop.common.DoorstopError` for unknown formats
562
563
    :return: lines generator if available
564
565
    """
566
    exts = ', '.join(ext for ext in FORMAT_LINES)
567
    msg = "unknown publish format: {} (options: {})".format(ext or None, exts)
568
    exc = DoorstopError(msg)
569
570
    try:
571
        gen = FORMAT_LINES[ext]
572
    except KeyError:
573
        raise exc from None
574
    else:
575
        log.debug("found lines generator for: {}".format(ext))
576
        return gen
577