PixarAnimationStudios / OpenTimelineIO

@@ -0,0 +1,302 @@
Loading
1 +
#!/usr/bin/env python
2 +
#
3 +
# Copyright 2019 Pixar Animation Studios
4 +
#
5 +
# Licensed under the Apache License, Version 2.0 (the "Apache License")
6 +
# with the following modification; you may not use this file except in
7 +
# compliance with the Apache License and the following modification to it:
8 +
# Section 6. Trademarks. is deleted and replaced with:
9 +
#
10 +
# 6. Trademarks. This License does not grant permission to use the trade
11 +
#    names, trademarks, service marks, or product names of the Licensor
12 +
#    and its affiliates, except as required to comply with Section 4(c) of
13 +
#    the License and to reproduce the content of the NOTICE file.
14 +
#
15 +
# You may obtain a copy of the Apache License at
16 +
#
17 +
#     http://www.apache.org/licenses/LICENSE-2.0
18 +
#
19 +
# Unless required by applicable law or agreed to in writing, software
20 +
# distributed under the Apache License with the above modification is
21 +
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
22 +
# KIND, either express or implied. See the Apache License for the specific
23 +
# language governing permissions and limitations under the Apache License.
24 +
#
25 +
26 +
27 +
"""Generates documentation of the serialized data model for OpenTimelineIO."""
28 +
29 +
import argparse
30 +
import inspect
31 +
import json
32 +
import tempfile
33 +
import sys
34 +
35 +
try:
36 +
    # python2
37 +
    import StringIO as io
38 +
except ImportError:
39 +
    # python3
40 +
    import io
41 +
42 +
import opentimelineio as otio
43 +
44 +
45 +
DOCUMENT_HEADER = """# OpenTimelineIO Serialized Data Documentation
46 +
47 +
This document is a list of all the OpenTimelineIO classes that serialize to and
48 +
from JSON, omitting SchemaDef plugins.
49 +
50 +
This document is automatically generated by running
51 +
 docs/autogen_serialized_datamodel.py, or by running `make doc-model`.  It is
52 +
 part of the unit tests suite and should be updated whenever the schema changes.
53 +
 If it needs to be updated, run: `make doc-model-update` and this file should be
54 +
 regenerated.
55 +
56 +
# Classes
57 +
58 +
"""
59 +
60 +
FIELDS_ONLY_HEADER = """# OpenTimelineIO Serialized Data Documentation
61 +
62 +
This document is a list of all the OpenTimelineIO classes that serialize to and
63 +
from JSON, omitting plugins classes and docstrings.
64 +
65 +
This document is automatically generated by running
66 +
 docs/autogen_serialized_datamodel.py, or by running `make doc-model`.  It is
67 +
 part of the unit tests suite and should be updated whenever the schema changes.
68 +
 If it needs to be updated, run: `make doc-model-update` and this file should be
69 +
 regenerated.
70 +
71 +
# Classes
72 +
73 +
"""
74 +
75 +
CLASS_HEADER_WITH_DOCS = """
76 +
### {classname}
77 +
78 +
*full module path*: `{modpath}`
79 +
80 +
*documentation*:
81 +
82 +
```
83 +
{docstring}
84 +
```
85 +
86 +
parameters:
87 +
"""
88 +
89 +
CLASS_HEADER_ONLY_FIELDS = """
90 +
### {classname}
91 +
92 +
parameters:
93 +
"""
94 +
95 +
MODULE_HEADER = """
96 +
## Module: {modname}
97 +
"""
98 +
99 +
PROP_HEADER = """- *{propkey}*: {prophelp}
100 +
"""
101 +
102 +
# @TODO: having type information here would be awesome
103 +
PROP_HEADER_NO_HELP = """- *{propkey}*
104 +
"""
105 +
106 +
# three ways to try and get the property + docstring
107 +
PROP_FETCHERS = (
108 +
    lambda cl, k: inspect.getdoc(getattr(cl, k)),
109 +
    lambda cl, k: inspect.getdoc(getattr(cl, "_" + k)),
110 +
    lambda cl, k: inspect.getdoc(getattr(cl(), k)),
111 +
)
112 +
113 +
114 +
def _parsed_args():
115 +
    """ parse commandline arguments with argparse """
116 +
117 +
    parser = argparse.ArgumentParser(
118 +
        description=__doc__,
119 +
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
120 +
    )
121 +
    group = parser.add_mutually_exclusive_group()
122 +
    group.add_argument(
123 +
        "-d",
124 +
        "--dryrun",
125 +
        action="store_true",
126 +
        default=False,
127 +
        help="Dryrun mode - print out instead of perform actions"
128 +
    )
129 +
    group.add_argument(
130 +
        "-o",
131 +
        "--output",
132 +
        type=str,
133 +
        default=None,
134 +
        help="Update the baseline with the current version"
135 +
    )
136 +
137 +
    return parser.parse_args()
138 +
139 +
140 +
# things to skip
141 +
SKIP_CLASSES = [otio.core.SerializableObject, otio.core.UnknownSchema]
142 +
SKIP_KEYS = ["OTIO_SCHEMA"]  # not data, just for the backing format
143 +
SKIP_MODULES = ["opentimelineio.schemadef"]  # because these are plugins
144 +
145 +
146 +
def _generate_model_for_module(mod, classes, modules):
147 +
    modules.add(mod)
148 +
149 +
    # fetch the classes from this module
150 +
    serializeable_classes = [
151 +
        thing for thing in mod.__dict__.values()
152 +
        if (
153 +
            inspect.isclass(thing)
154 +
            and thing not in classes
155 +
            and issubclass(thing, otio.core.SerializableObject)
156 +
            or thing in (
157 +
                otio.opentime.RationalTime,
158 +
                otio.opentime.TimeRange,
159 +
                otio.opentime.TimeTransform,
160 +
            )
161 +
        )
162 +
    ]
163 +
164 +
    # serialize/deserialize the classes to capture their serialized parameters
165 +
    model = {}
166 +
    for cl in serializeable_classes:
167 +
        if cl in SKIP_CLASSES:
168 +
            continue
169 +
170 +
        model[cl] = {}
171 +
        field_dict = json.loads(otio.adapters.otio_json.write_to_string(cl()))
172 +
        for k in field_dict.keys():
173 +
            if k in SKIP_KEYS:
174 +
                continue
175 +
176 +
            for fetcher in PROP_FETCHERS:
177 +
                try:
178 +
                    model[cl][k] = fetcher(cl, k)
179 +
                    break
180 +
                except AttributeError:
181 +
                    pass
182 +
            else:
183 +
                sys.stderr.write("ERROR: could not fetch property: {}".format(k))
184 +
185 +
        # Stashing the OTIO_SCHEMA back into the dictionary since the
186 +
        # documentation uses this information in its header.
187 +
        model[cl]["OTIO_SCHEMA"] = field_dict["OTIO_SCHEMA"]
188 +
189 +
    classes.update(model)
190 +
191 +
    # find new modules to recurse into
192 +
    new_mods = sorted(
193 +
        (
194 +
            thing for thing in mod.__dict__.values()
195 +
            if (
196 +
                inspect.ismodule(thing)
197 +
                and thing not in modules
198 +
                and all(not thing.__name__.startswith(t) for t in SKIP_MODULES)
199 +
            )
200 +
        ),
201 +
        key=lambda mod: str(mod)
202 +
    )
203 +
204 +
    # recurse into the new modules and update the classes and modules values
205 +
    [_generate_model_for_module(m, classes, modules) for m in new_mods]
206 +
207 +
208 +
def _generate_model():
209 +
    classes = {}
210 +
    modules = set()
211 +
    _generate_model_for_module(otio, classes, modules)
212 +
    return classes
213 +
214 +
215 +
def _write_documentation(model):
216 +
    md_with_helpstrings = io.StringIO()
217 +
    md_only_fields = io.StringIO()
218 +
219 +
    md_with_helpstrings.write(DOCUMENT_HEADER)
220 +
    md_only_fields.write(FIELDS_ONLY_HEADER)
221 +
222 +
    modules = {}
223 +
    for cl in model:
224 +
        modules.setdefault(cl.__module__, []).append(cl)
225 +
226 +
    CURRENT_MODULE = None
227 +
    for module_list in sorted(modules):
228 +
        this_mod = ".".join(module_list.split('.')[:2])
229 +
        if this_mod != CURRENT_MODULE:
230 +
            CURRENT_MODULE = this_mod
231 +
            md_with_helpstrings.write(MODULE_HEADER.format(modname=this_mod))
232 +
            md_only_fields.write(MODULE_HEADER.format(modname=this_mod))
233 +
234 +
        # because these are classes, they need to sort on their stringified
235 +
        # names
236 +
        for cl in sorted(modules[module_list], key=lambda cl: str(cl)):
237 +
            modname = inspect.getmodule(cl).__name__
238 +
            label = model[cl]["OTIO_SCHEMA"]
239 +
            md_with_helpstrings.write(
240 +
                CLASS_HEADER_WITH_DOCS.format(
241 +
                    classname=label,
242 +
                    modpath=modname + "." + cl.__name__,
243 +
                    docstring=cl.__doc__
244 +
                )
245 +
            )
246 +
            md_only_fields.write(
247 +
                CLASS_HEADER_ONLY_FIELDS.format(
248 +
                    classname=label,
249 +
                )
250 +
            )
251 +
252 +
            for key, helpstr in sorted(model[cl].items()):
253 +
                if key in SKIP_KEYS:
254 +
                    continue
255 +
                md_with_helpstrings.write(
256 +
                    PROP_HEADER.format(propkey=key, prophelp=helpstr)
257 +
                )
258 +
                md_only_fields.write(
259 +
                    PROP_HEADER_NO_HELP.format(propkey=key)
260 +
                )
261 +
262 +
    return md_with_helpstrings.getvalue(), md_only_fields.getvalue()
263 +
264 +
265 +
def main():
266 +
    """  main entry point  """
267 +
    args = _parsed_args()
268 +
    with_docs, without_docs = generate_and_write_documentation()
269 +
270 +
    # print it out somewhere
271 +
    if args.dryrun:
272 +
        print(with_docs)
273 +
        return
274 +
275 +
    output = args.output
276 +
    if not output:
277 +
        output = tempfile.NamedTemporaryFile(
278 +
            'w',
279 +
            suffix="otio_serialized_schema.md",
280 +
            delete=False
281 +
        ).name
282 +
283 +
    with open(output, 'w') as fo:
284 +
        fo.write(with_docs)
285 +
286 +
    # write version without docstrings
287 +
    prefix, suffix = output.rsplit('.', 1)
288 +
    output_only_fields = prefix + "-only-fields." + suffix
289 +
290 +
    with open(output_only_fields, 'w') as fo:
291 +
        fo.write(without_docs)
292 +
293 +
    print("wrote documentation to {} and {}".format(output, output_only_fields))
294 +
295 +
296 +
def generate_and_write_documentation():
297 +
    model = _generate_model()
298 +
    return _write_documentation(model)
299 +
300 +
301 +
if __name__ == '__main__':
302 +
    main()

@@ -35,5 +35,6 @@
Loading
35 35
    otiocat,
36 36
    otiostat,
37 37
    console_utils,
38 +
    autogen_serialized_datamodel,
38 39
)
39 40
Files Coverage
opentimelineio 92.75%
opentimelineio_contrib/adapters 85.22%
Project Totals (69 files) 88.66%
1119.2
TRAVIS_PYTHON_VERSION=3.6
TRAVIS_OS_NAME=linux
1119.1
TRAVIS_PYTHON_VERSION=2.7
TRAVIS_OS_NAME=linux
1
codecov:
2
  notify:
3
    require_ci_to_pass: yes
4

5
coverage:
6
  precision: 2
7
  round: down
8
  range: "70...100"
9

10
  status:
11
    project: yes
12
    patch: yes
13
    changes: no
14

15
comment:
16
  layout: "reach, diff, flags, files, footer"
17
  behavior: default
18
require_changes: no
19

20
ignore:
21
  - "opentimelineio_contrib/adapters/tests/test_rvsession.py"
22
  - "opentimelineio_contrib/adapters/tests/test_maya_sequencer.py"
23
  - "opentimelineio_contrib/adapters/tests/test_burnins.py"
24
  - "opentimelineio_contrib/adapters/burnins.py"
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading