Passed
Push — develop ( e91dc8...2ae66f )
by Jace
04:37 queued 14s
created

doorstop.core.publishers.base   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 323
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 59
eloc 185
dl 0
loc 323
rs 4.08
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A BasePublisher.getLinkify() 0 3 1
A BasePublisher.publishAction() 0 8 1
A BasePublisher._check_for_list_end() 0 13 4
A BasePublisher.setup() 0 14 4
A BasePublisher.getIndex() 0 3 1
A BasePublisher.preparePublish() 0 2 1
A BasePublisher.format_references() 0 4 1
A BasePublisher.getTemplate() 0 3 1
A BasePublisher.format_attr_list() 0 4 1
C BasePublisher.table_of_contents() 0 35 9
A BasePublisher.getAssetsPath() 0 3 1
F BasePublisher.process_lists() 0 57 14
A BasePublisher.format_ref() 0 4 1
A BasePublisher.format_links() 0 6 1
A BasePublisher.create_matrix() 0 3 1
A BasePublisher.lines() 0 11 1
A BasePublisher.create_index() 0 13 1
A BasePublisher.concludePublish() 0 2 1
A BasePublisher.setPath() 0 3 1
A BasePublisher.get_line_generator() 0 3 1
A BasePublisher.format_label_links() 0 6 1
A BasePublisher.getDocumentPath() 0 3 1
A BasePublisher.processTemplates() 0 4 1
A BasePublisher.getMatrix() 0 3 1
A BasePublisher.format_item_link() 0 6 1
A BasePublisher.__init__() 0 25 1

3 Functions

Rating   Name   Duplication   Size   Complexity  
A format_level() 0 6 3
A extract_uid() 0 6 2
A extract_prefix() 0 3 1

How to fix   Complexity   

Complexity

Complex classes like doorstop.core.publishers.base often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# SPDX-License-Identifier: LGPL-3.0-only
2
3
"""Abstract interface to publishers."""
4
5
from abc import ABCMeta, abstractmethod
6
from re import compile as re_compile
7
from typing import Any, Dict
8
9
from doorstop import common, settings
10
from doorstop.common import DoorstopError
11
from doorstop.core.template import get_template
12
from doorstop.core.types import is_tree, iter_items
13
14
log = common.logger(__name__)
15
16
17
class BasePublisher(metaclass=ABCMeta):
18
    """Abstract base class for publishers.
19
20
    All functions marked as @abstractmethod must be defined by the publisher
21
    class.
22
23
    All other functions are standard and _can_ be overridden _if needed_.
24
    """
25
26
    def __init__(self, obj, ext):
27
        """Initialize publisher class."""
28
        self.object = obj
29
        self.ext = ext
30
        self.path = ""
31
        self.document = None
32
        self.documentPath = ""
33
        self.assetsPath = ""
34
        self.template = ""
35
        self.linkify = None
36
        self.index = None
37
        self.matrix = None
38
        # Define lists.
39
        self.list: Dict[str, Dict[str, Any]] = {}
40
        self.list["depth"] = {"itemize": 0, "enumerate": 0}
41
        self.list["indent"] = {"itemize": 0, "enumerate": 0}
42
        self.list["found"] = {"itemize": False, "enumerate": False}
43
        # Create regexps.
44
        self.list["regexp"] = {
45
            "itemize": re_compile(r"^\s*[\*+-]\s(.*)"),
46
            "enumerate": re_compile(r"^\s*\d+\.\s(.*)"),
47
        }
48
        self.list["sub"] = {
49
            "itemize": re_compile(r"^\s*[\*+-]\s"),
50
            "enumerate": re_compile(r"^\s*\d+\.\s"),
51
        }
52
53
    def setup(self, linkify, index, matrix):
54
        """Check and store linkfy, index and matrix settings."""
55
        if linkify is None:
56
            self.linkify = is_tree(self.object) and self.ext in [".html", ".md", ".tex"]
57
        else:
58
            self.linkify = linkify
59
        if index is None:
60
            self.index = is_tree(self.object) and self.ext == ".html"
61
        else:
62
            self.index = index
63
        if matrix is None:
64
            self.matrix = is_tree(self.object)
65
        else:
66
            self.matrix = matrix
67
68
    def preparePublish(self):
69
        """Prepare publish.
70
71
        Replace this with code that should be run _before_ a document or tree
72
        is published.
73
        """
74
75
    def publishAction(self, document, path):
76
        """Publish action.
77
78
        Replace this with code that should be run _for each_ document
79
        _during_ publishing.
80
        """
81
        self.document = document
82
        self.documentPath = path
83
84
    def concludePublish(self):
85
        """Conclude publish.
86
87
        Replace this with code that should be run _after_ a document or tree
88
        is published.
89
        """
90
91
    def table_of_contents(self, linkify=None, obj=None):
92
        toc = "### Table of Contents\n\n"
93
        if obj is None:
94
            toc_doc = self.object
95
        else:
96
            toc_doc = obj
97
98
        for item in iter_items(toc_doc):
99
            if item.depth == 1:
100
                prefix = " * "
101
            else:
102
                prefix = "    " * (item.depth - 1)
103
                prefix += "* "
104
105
            # Check if item has the attribute heading.
106
            if item.heading:
107
                lines = item.text.splitlines()
108
                heading = lines[0] if lines else ""
109
            elif item.header:
110
                heading = "{h}".format(h=item.header)
111
            else:
112
                heading = item.uid
113
114
            if settings.PUBLISH_HEADING_LEVELS:
115
                level = format_level(item.level)
116
                lbl = "{lev} {h}".format(lev=level, h=heading)
117
            else:
118
                lbl = heading
119
120
            if linkify:
121
                line = "{p}[{lbl}](#{uid})\n".format(p=prefix, lbl=lbl, uid=item.uid)
122
            else:
123
                line = "{p}{lbl}\n".format(p=prefix, lbl=lbl)
124
            toc += line
125
        return toc
126
127
    @abstractmethod
128
    def lines(self, obj, **kwargs):  # pragma: no cover (abstract method)
129
        """Yield lines for a report.
130
131
        :param obj: Item, list of Items, or Document to publish
132
        :param linkify: turn links into hyperlinks
133
134
        :return: iterator of lines of text
135
136
        """
137
        raise NotImplementedError
138
139
    @abstractmethod
140
    def create_index(
141
        self, directory, index=None, extensions=(".html",), tree=None
142
    ):  # pragma: no cover (abstract method)
143
        """Create an index of all files in a directory.
144
145
        :param directory: directory for index
146
        :param index: filename for index
147
        :param extensions: file extensions to include
148
        :param tree: optional tree to determine index structure
149
150
        """
151
        raise NotImplementedError
152
153
    def create_matrix(self, directory):  # pragma: no cover (abstract method)
154
        """Create a traceability table."""
155
        raise NotImplementedError
156
157
    @abstractmethod
158
    def format_attr_list(self, item, linkify):  # pragma: no cover (abstract method)
159
        """Create an attribute list for a heading."""
160
        raise NotImplementedError
161
162
    @abstractmethod
163
    def format_ref(self, item):  # pragma: no cover (abstract method)
164
        """Format an external reference."""
165
        raise NotImplementedError
166
167
    @abstractmethod
168
    def format_references(self, item):  # pragma: no cover (abstract method)
169
        """Format an external reference."""
170
        raise NotImplementedError
171
172
    @abstractmethod
173
    def format_links(
174
        self, items, linkify, to_html=False
175
    ):  # pragma: no cover (abstract method)
176
        """Format a list of linked items."""
177
        raise NotImplementedError
178
179
    @abstractmethod
180
    def format_item_link(
181
        self, item, linkify=True
182
    ):  # pragma: no cover (abstract method)
183
        """Format an item link."""
184
        raise NotImplementedError
185
186
    @abstractmethod
187
    def format_label_links(
188
        self, label, links, linkify
189
    ):  # pragma: no cover (abstract method)
190
        """Join a string of label and links with formatting."""
191
        raise NotImplementedError
192
193
    def get_line_generator(self):
194
        """Return the lines generator for the class."""
195
        return self.lines
196
197
    def setPath(self, path):
198
        """Set the export path of the tree and/or document."""
199
        self.path = path
200
201
    def getDocumentPath(self):
202
        """Get the export path of the individual document."""
203
        return self.documentPath
204
205
    def processTemplates(self, template):
206
        """Retrieve the template and its path."""
207
        self.assetsPath, self.template = get_template(
208
            self.object, self.path, self.ext, template
209
        )
210
211
    def getAssetsPath(self):
212
        """Get the assets path of the individual document."""
213
        return self.assetsPath
214
215
    def getTemplate(self):
216
        """Get the template."""
217
        return self.template
218
219
    def getIndex(self):
220
        """Get the index flag."""
221
        return self.index
222
223
    def getMatrix(self):
224
        """Get the matrix flag."""
225
        return self.matrix
226
227
    def getLinkify(self):
228
        """Get the linkify flag."""
229
        return self.linkify
230
231
    def process_lists(self, line, next_line):
232
        """Process lists in the line. Intended for LaTeX and HTML publishers."""
233
        # Don't process custom attributes.
234
        if "CUSTOM-ATTRIB" in line:
235
            return (False, "", line)
236
        # Loop over both list types.
237
        for temp_type in ["itemize", "enumerate"]:
238
            matches = self.list["regexp"][temp_type].findall(line)
239
            if matches:
240
                list_type = temp_type
241
                # Cannot have both types on the same line.
242
                break
243
        block = []
244
        no_paragraph = False
245
        if matches:
0 ignored issues
show
introduced by
The variable matches does not seem to be defined in case the for loop on line 237 is not entered. Are you sure this can never be the case?
Loading history...
246
            indent = len(line) - len(line.lstrip())
247
            if not self.list["found"][list_type]:
0 ignored issues
show
introduced by
The variable list_type does not seem to be defined for all execution paths.
Loading history...
248
                block.append(self.list["start"][list_type])
249
                self.list["found"][list_type] = True
250
                self.list["depth"][list_type] = indent
251
            elif self.list["depth"][list_type] < indent:
252
                block.append(self.list["start"][list_type])
253
                if self.list["depth"][list_type] == 0:
254
                    self.list["indent"][list_type] = indent
255
                elif (
256
                    self.list["depth"][list_type] + self.list["indent"][list_type]
257
                    != indent
258
                ):
259
                    raise DoorstopError(
260
                        "Cannot change indentation depth inside a list."
261
                    )
262
                self.list["depth"][list_type] = indent
263
            elif self.list["depth"][list_type] > indent:
264
                while self.list["depth"][list_type] > indent:
265
                    block.append(self.list["end"][list_type])
266
                    self.list["depth"][list_type] = (
267
                        self.list["depth"][list_type] - self.list["indent"][list_type]
268
                    )
269
        # Check both list types.
270
        for list_type in ["itemize", "enumerate"]:
271
            if self.list["found"][list_type]:
272
                no_paragraph = True
273
                # Replace the list identifier.
274
                line = (
275
                    self.list["sub"][list_type].sub(
276
                        self.list["start_item"][list_type], line
277
                    )
278
                    + self.list["end_item"][list_type]
279
                )
280
                # Look ahead - need empty line to end itemize!
281
                (block, line) = self._check_for_list_end(
282
                    line, next_line, block, list_type
283
                )
284
        if len(block) > 0:
285
            return (no_paragraph, "\n".join(block), line)
286
        else:
287
            return (no_paragraph, "", line)
288
289
    def _check_for_list_end(self, line, next_line, block, list_type):
290
        """Check if the list has ended."""
291
        if next_line == "" or next_line.startswith("<p>"):
292
            block.append(line)
293
            while self.list["depth"][list_type] > 0:
294
                block.append(self.list["end"][list_type])
295
                self.list["depth"][list_type] = (
296
                    self.list["depth"][list_type] - self.list["indent"][list_type]
297
                )
298
            line = self.list["end"][list_type]
299
            self.list["found"][list_type] = False
300
            self.list["depth"][list_type] = 0
301
        return (block, line)
302
303
304
def extract_prefix(document):
305
    """Return the document prefix."""
306
    return document.prefix
307
308
309
def extract_uid(item):
310
    """Return the item uid."""
311
    if item:
312
        return item.uid
313
    else:
314
        return None
315
316
317
def format_level(level):
318
    """Convert a level to a string and keep zeros if not a top level."""
319
    text = str(level)
320
    if text.endswith(".0") and len(text) > 3:
321
        text = text[:-2]
322
    return text
323