Skip to content

Lint

Check site consistency.

lint(opt)

Main driver.

Source code in mccole/lint.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def lint(opt):
    """Main driver."""
    files = find_files(opt, {opt.out})
    check_file_references(files)

    files = {path: data for path, data in files.items() if path.suffix == ".md"}
    extras = {
        "bibliography": find_key_defs(files, "bibliography"),
        "glossary": find_key_defs(files, "glossary"),
    }

    linters = [
        lint_bibliography_references,
        lint_figure_numbers,
        lint_figure_references,
        lint_glossary_redefinitions,
        lint_glossary_references,
        lint_link_definitions,
        lint_markdown_links,
        lint_table_references,
    ]
    sections = {path: data["content"] for path, data in files.items()}
    if all(list(f(opt, sections, extras) for f in linters)):
        print("All self-checks passed.")

check_file_references(files)

Check inter-file references.

Source code in mccole/lint.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def check_file_references(files):
    """Check inter-file references."""
    ok = True
    for path, data in files.items():
        content = data["content"]
        if path.suffix != ".md":
            continue
        for link in MD_FILE_LINK.finditer(content):
            if _is_special_link(link.group(2)):
                continue
            target = _resolve_path(path.parent, link.group(2))
            if _is_missing(target, files):
                print(f"Missing file: {path} => {target}")
                ok = False
    return ok

lint_bibliography_references(opt, sections, extras)

Check bibliography references.

Source code in mccole/lint.py
65
66
67
68
69
70
71
def lint_bibliography_references(opt, sections, extras):
    """Check bibliography references."""
    available = set(extras["bibliography"].keys())
    if available is None:
        print("No bibliography found (or multiple matches)")
        return False
    return _check_references(sections, "bibliography", BIB_REF, available)

lint_figure_numbers(opt, sections, extras)

Check figure numbering.

Source code in mccole/lint.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def lint_figure_numbers(opt, sections, extras):
    """Check figure numbering."""
    ok = True
    for path, content in sections.items():
        current = 1
        for caption in FIGURE_CAPTION.finditer(content):
            text = caption.group(1)
            if ("Figure" not in text) or (":" not in text):
                print(f"Bad caption: {path} / '{text}'")
                ok = False
                continue
            fields = text.split(":")
            if len(fields) != 2:
                print(f"Bad caption: {path} / '{text}'")
                ok = False
                continue
            fields = fields[0].split(" ")
            if len(fields) != 2:
                print(f"Bad caption: {path} / '{text}'")
                ok = False
                continue
            try:
                number = int(fields[1])
                if number != current:
                    print(f"Caption number out of sequence: {path} / '{text}'")
                    ok = False
                else:
                    current += 1
            except ValueError:
                print(f"Bad caption number: {path} / '{text}'")
                ok = False
    return ok

lint_figure_references(opt, sections, extras)

Check figure references.

Source code in mccole/lint.py
108
109
110
def lint_figure_references(opt, sections, extras):
    """Check figure references."""
    return _check_object_refs(sections, "figure", FIGURE_DEF, FIGURE_REF)

lint_glossary_redefinitions(opt, sections, extras)

Check glossary redefinitions.

Source code in mccole/lint.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def lint_glossary_redefinitions(opt, sections, extras):
    """Check glossary redefinitions."""
    found = defaultdict(set)
    for path, content in sections.items():
        if "glossary" in str(path).lower():
            continue
        for m in GLOSS_REF.finditer(content):
            found[m[1]].add(str(path))

    problems = {k:v for k, v in found.items() if len(v) > 1}
    for k, v in problems.items():
        if len(v) > 1:
            print(f"glossary key {k} redefined: {', '.join(sorted(v))}")
    return len(problems) == 0

lint_glossary_references(opt, sections, extras)

Check glossary references.

Source code in mccole/lint.py
129
130
131
132
133
134
135
def lint_glossary_references(opt, sections, extras):
    """Check glossary references."""
    available = set(extras["glossary"].keys())
    if available is None:
        print("No glossary found (or multiple matches)")
        return False
    return _check_references(sections, "glossary", GLOSS_REF, available)

Check that Markdown files define the links they use.

Source code in mccole/lint.py
138
139
140
141
142
143
144
145
def lint_link_definitions(opt, sections, extras):
    """Check that Markdown files define the links they use."""
    ok = True
    for path, content in sections.items():
        link_refs = {m[1] for m in MD_LINK_REF.findall(content)}
        link_defs = {m[0] for m in MD_LINK_DEF.findall(content)}
        ok = ok and _report_diff(f"{path} links", link_refs, link_defs)
    return ok

Check consistency of Markdown links.

Source code in mccole/lint.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def lint_markdown_links(opt, sections, extras):
    """Check consistency of Markdown links."""
    found = defaultdict(lambda: defaultdict(set))
    for path, content in sections.items():
        for link in MD_LINK_DEF.finditer(content):
            label, url = link.group(1), link.group(2)
            found[label][url].add(path)

    ok = True
    for label, data in found.items():
        if len(data) > 1:
            info = {str(k):", ".join(sorted(str(p) for p in v)) for k, v in data.items()}
            msg = ", ".join(f"{k} in {v}" for k, v in info.items())
            print(f"Inconsistent link {label}: {msg}")
            ok = False
    return ok

lint_table_references(opt, sections, extras)

Check figure references.

Source code in mccole/lint.py
166
167
168
def lint_table_references(opt, sections, extras):
    """Check figure references."""
    return _check_object_refs(sections, "table", TABLE_DEF, TABLE_REF)

parse_args(parser)

Parse command-line arguments.

Source code in mccole/lint.py
171
172
173
174
175
176
def parse_args(parser):
    """Parse command-line arguments."""
    parser.add_argument("--config", type=str, default="pyproject.toml", help="optional configuration file")
    parser.add_argument("--out", type=str, default="docs", help="output directory")
    parser.add_argument("--root", type=str, default=".", help="root directory")
    return parser

_check_object_refs(sections, kind, pattern_def, pattern_ref)

Check for figure and table references within each Markdown file.

Source code in mccole/lint.py
179
180
181
182
183
184
185
186
def _check_object_refs(sections, kind, pattern_def, pattern_ref):
    """Check for figure and table references within each Markdown file."""
    ok = True
    for path, content in sections.items():
        defined = set(pattern_def.findall(content))
        referenced = set(pattern_def.findall(content))
        ok = _report_diff(f"{path} {kind}", referenced, defined) and ok
    return ok

_check_references(sections, term, regexp, available)

Check all Markdown files for cross-references.

Source code in mccole/lint.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def _check_references(sections, term, regexp, available):
    """Check all Markdown files for cross-references."""
    ok = True
    seen = set()
    for path, content in sections.items():
        found = {k.group(1) for k in regexp.finditer(content)}
        seen |= found
        missing = found - available
        if missing:
            print(f"Missing {term} keys in {path}: {', '.join(sorted(missing))}")
            ok = False

    unused = available - seen
    if unused:
        print(f"Unused {term} keys: {', '.join(sorted(unused))}")
        ok = False

    return ok

_is_missing(actual, available)

Is a file missing?

Source code in mccole/lint.py
209
210
211
212
213
def _is_missing(actual, available):
    """Is a file missing?"""
    return (not actual.exists()) or (
        (actual.suffix in SUFFIXES) and (actual not in available)
    )

Is this link handled specially?

Source code in mccole/lint.py
216
217
218
def _is_special_link(link):
    """Is this link handled specially?"""
    return link.startswith("b:") or link.startswith("g:")

_report_diff(msg, refs, defs)

Report differences if any.

Source code in mccole/lint.py
221
222
223
224
225
226
227
228
def _report_diff(msg, refs, defs):
    """Report differences if any."""
    ok = True
    for (kind, vals) in (("missing", refs - defs), ("unused", defs - refs)):
        if vals:
            print(f"{msg} {kind}: {', '.join(vals)}")
            ok = False
    return ok

_resolve_path(source, dest)

Account for '..' in paths.

Source code in mccole/lint.py
231
232
233
234
235
236
237
def _resolve_path(source, dest):
    """Account for '..' in paths."""
    while dest[:3] == "../":
        source = source.parent
        dest = dest[3:]
    result = Path(source, dest)
    return result