File: lv2_check_specification.py

package info (click to toggle)
lv2 1.18.10-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,056 kB
  • sloc: ansic: 5,684; python: 1,746; xml: 158; sh: 92; cpp: 48; makefile: 9
file content (248 lines) | stat: -rwxr-xr-x 7,738 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#!/usr/bin/env python3

# Copyright 2020-2022 David Robillard <d@drobilla.net>
# SPDX-License-Identifier: ISC

"""
Check an LV2 specification for issues.
"""

import argparse
import os
import sys

import rdflib

foaf = rdflib.Namespace("http://xmlns.com/foaf/0.1/")
lv2 = rdflib.Namespace("http://lv2plug.in/ns/lv2core#")
owl = rdflib.Namespace("http://www.w3.org/2002/07/owl#")
rdf = rdflib.Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
rdfs = rdflib.Namespace("http://www.w3.org/2000/01/rdf-schema#")


class Checker:
    "A callable that checks conditions and records pass/fail counts."

    def __init__(self, verbose=False):
        self.num_checks = 0
        self.num_errors = 0
        self.verbose = verbose

    def __call__(self, condition, name):
        if not condition:
            sys.stderr.write(f"error: Unmet condition: {name}\n")
            self.num_errors += 1
        elif self.verbose:
            sys.stderr.write(f"note: {name}\n")

        self.num_checks += 1
        return condition

    def print_summary(self):
        "Print a summary (if verbose) when all checks are finished."

        if self.verbose:
            if self.num_errors:
                sys.stderr.write(f"note: Failed {self.num_errors}/")
            else:
                sys.stderr.write("note: Passed all ")

            sys.stderr.write(f"{self.num_checks} checks\n")


def _check(condition, name):
    "Check that condition is true, returning 1 on failure."

    if not condition:
        sys.stderr.write(f"error: Unmet condition: {name}\n")
        return 1

    return 0


def _has_statement(model, pattern):
    "Return true if model contains a triple matching pattern."

    for _ in model.triples(pattern):
        return True

    return False


def _has_property(model, subject, predicate):
    "Return true if subject has any value for predicate in model."

    return model.value(subject, predicate, None) is not None


def _check_version(checker, model, spec, is_stable):
    "Check that the version of a specification is present and valid."

    minor = model.value(spec, lv2.minorVersion, None, any=False)
    checker(minor is not None, f"{spec} has a lv2:minorVersion")

    micro = model.value(spec, lv2.microVersion, None, any=False)
    checker(micro is not None, f"{spec} has a lv2:microVersion")

    if is_stable:
        checker(int(minor) > 0, f"{spec} has a non-zero minor version")
        checker(int(micro) % 2 == 0, f"{spec} has an even micro version")


def _check_specification(checker, spec_dir, is_stable=False):
    "Check all specification data for errors and omissions."

    # Load manifest
    manifest_path = os.path.join(spec_dir, "manifest.ttl")
    model = rdflib.Graph()
    model.parse(manifest_path, format="n3")

    # Get the specification URI from the manifest
    spec_uri = model.value(None, rdf.type, lv2.Specification, any=False)
    if not checker(
        spec_uri is not None,
        manifest_path + " declares an lv2:Specification",
    ):
        return 1

    # Check that the manifest declares a valid version
    _check_version(checker, model, spec_uri, is_stable)

    # Get the link to the main document from the manifest
    document = model.value(spec_uri, rdfs.seeAlso, None, any=False)
    if not checker(
        document is not None,
        manifest_path + " has one rdfs:seeAlso link to the definition",
    ):
        return 1

    # Load main document into the model
    model.parse(document, format="n3")

    # Check that the main data files aren't bloated with extended documentation
    checker(
        not _has_statement(model, [None, lv2.documentation, None]),
        f"{document} has no lv2:documentation",
    )

    # Load all other directly linked data files (for any other subjects)
    for link in sorted(model.triples([None, rdfs.seeAlso, None])):
        if link[2] != document and link[2].endswith(".ttl"):
            model.parse(link[2], format="n3")

    # Check that all properties have a more specific type
    for typing in sorted(model.triples([None, rdf.type, rdf.Property])):
        subject = typing[0]

        checker(isinstance(subject, rdflib.term.URIRef), f"{subject} is a URI")

        if str(subject) == "http://lv2plug.in/ns/ext/patch#value":
            continue  # patch:value is just a "promiscuous" rdf:Property

        types = list(model.objects(subject, rdf.type))

        checker(
            (owl.DatatypeProperty in types)
            or (owl.ObjectProperty in types)
            or (owl.AnnotationProperty in types),
            f"{subject} is a Datatype, Object, or Annotation property",
        )

    # Get all subjects that have an explicit rdf:type
    typed_subjects = set()
    for typing in model.triples([None, rdf.type, None]):
        typed_subjects.add(typing[0])

    # Check that all named and typed resources have labels and comments
    for subject in typed_subjects:
        if isinstance(
            subject, rdflib.term.BNode
        ) or foaf.Person in model.objects(subject, rdf.type):
            continue

        if checker(
            _has_property(model, subject, rdfs.label),
            f"{subject} has a rdfs:label",
        ):
            label = str(model.value(subject, rdfs.label, None))

            checker(
                not label.endswith("."),
                f"{subject} label has no trailing '.'",
            )
            checker(
                label.find("\n") == -1,
                f"{subject} label is a single line",
            )
            checker(
                label == label.strip(),
                f"{subject} label has stripped whitespace",
            )

        if checker(
            _has_property(model, subject, rdfs.comment),
            f"{subject} has a rdfs:comment",
        ):
            comment = str(model.value(subject, rdfs.comment, None))

            checker(
                comment.endswith("."),
                f"{subject} comment has a trailing '.'",
            )
            checker(
                comment.find("\n") == -1 and comment.find("\r"),
                f"{subject} comment is a single line",
            )
            checker(
                comment == comment.strip(),
                f"{subject} comment has stripped whitespace",
            )

        # Check that lv2:documentation, if present, is proper Markdown
        documentation = model.value(subject, lv2.documentation, None)
        if documentation is not None:
            checker(
                documentation.datatype == lv2.Markdown,
                f"{subject} documentation is explicitly Markdown",
            )
            checker(
                str(documentation).startswith("\n\n"),
                f"{subject} documentation starts with blank line",
            )
            checker(
                str(documentation).endswith("\n\n"),
                f"{subject} documentation ends with blank line",
            )

    return checker.num_errors


if __name__ == "__main__":
    ap = argparse.ArgumentParser(
        usage="%(prog)s [OPTION]... BUNDLE",
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )

    ap.add_argument(
        "--stable",
        action="store_true",
        help="enable checks for stable release versions",
    )

    ap.add_argument(
        "-v", "--verbose", action="store_true", help="print successful checks"
    )

    ap.add_argument(
        "BUNDLE", help="path to specification bundle or manifest.ttl"
    )

    args = ap.parse_args(sys.argv[1:])

    if os.path.basename(args.BUNDLE):
        args.BUNDLE = os.path.dirname(args.BUNDLE)

    sys.exit(
        _check_specification(Checker(args.verbose), args.BUNDLE, args.stable)
    )