doorstop.core.publishers.base   F
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 351
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 65
eloc 203
dl 0
loc 351
rs 3.2
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A BasePublisher.getLinkify() 0 3 1
A BasePublisher.publishAction() 0 12 2
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
A BasePublisher.table_of_contents() 0 13 1
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 4 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

4 Functions

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

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