From 0f9a4262dc433e13711fe932c3be45e329123c92 Mon Sep 17 00:00:00 2001 From: Stephan Steinbach <61017+ssteinbach@users.noreply.github.com> Date: Tue, 16 Jul 2019 15:16:45 -0700 Subject: [PATCH] Autogenerate documentation of the serialized data model (#521) * Add autogen_serialized_datamodel script. * Add unit test * Add the serialized schema docs to the manual. * Add in docstrings and label versions. * Add link to autogenerated file specification. * Update path in Makefile. * Also get properties for opentime classes. * Remove old schema documentation * Add second doc that doesn't have docstrings - second doc isn't linked by the documentation, and only has lists of the fields * Add copyright notice. --- Makefile | 5 + docs/index.rst | 1 + .../otio-file-format-specification.md | 62 +- .../otio-serialized-schema-only-fields.md | 249 ++++++++ docs/tutorials/otio-serialized-schema.md | 548 ++++++++++++++++++ opentimelineio/console/__init__.py | 1 + .../console/autogen_serialized_datamodel.py | 302 ++++++++++ setup.py | 1 + tests/test_serialized_schema.py | 51 ++ 9 files changed, 1159 insertions(+), 61 deletions(-) create mode 100644 docs/tutorials/otio-serialized-schema-only-fields.md create mode 100644 docs/tutorials/otio-serialized-schema.md create mode 100644 opentimelineio/console/autogen_serialized_datamodel.py create mode 100644 tests/test_serialized_schema.py diff --git a/Makefile b/Makefile index a01979db8..a1b3add53 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,11 @@ ifndef FLAKE8_PROG endif @python -m flake8 +doc-model: + @python opentimelineio/console/autogen_serialized_datamodel.py --dryrun + +doc-model-update: + @python opentimelineio/console/autogen_serialized_datamodel.py -o docs/tutorials/otio-serialized-schema.md # generate documentation in html doc-html: diff --git a/docs/index.rst b/docs/index.rst index d3686b2e3..f4a77144e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,6 +41,7 @@ Tutorials tutorials/contributing tutorials/feature-matrix tutorials/otio-file-format-specification + tutorials/otio-serialized-schema tutorials/otio-timeline-structure tutorials/time-ranges tutorials/write-an-adapter diff --git a/docs/tutorials/otio-file-format-specification.md b/docs/tutorials/otio-file-format-specification.md index f03ed4f6b..830c8b540 100644 --- a/docs/tutorials/otio-file-format-specification.md +++ b/docs/tutorials/otio-file-format-specification.md @@ -215,64 +215,4 @@ TODO: Explain how metadata works and why we do it that way. ## Schema Specification -Each OTIO data type (schema) defines its own key/value pairs. Here are the details of each. - -This list has the expanded fields of each concrete object type that you are likely to encounter in an OTIO file. It does not have the intermediate parent classes (Composition, Item, SerializableObject, etc.) - -### Timeline.1 -* name (string) -* tracks (Stack) -* global_start_time (RationalTime) -* metadata (dict) - -### Sequence.1 -* name (string) -* children (any Composable objects, like Clip, Transition, Filler, Stack, Sequence) -* metadata (dict) -* kind (string enumeration SequenceKind.Video, etc.) -* source_range (TimeRange, may be null) -* effects (list of Effect) -* markers (list of Markers) - -### Stack.1 -* name (string) -* children (any Composable objects, like Clip, Transition, Filler, Stack, Sequence) -* metadata (dict) -* source_range (TimeRange, may be null) -* effects (list of Effect) -* markers (list of Markers) - -### Clip.1 -* name (string) -* metadata (dict) -* source_range (TimeRange, may be null) -* effects (list of Effect) -* markers (list of Markers) -* media_reference (MediaReference, may be null) - -### MediaReference.1 -* name (string) -* available_range (TimeRange, may be null) -* metadata (dict) - -### Filler.1 -* name (string) -* metadata (dict) -* source_range (TimeRange, may be null) -* effects (list of Effect) -* markers (list of Markers) - -### Transition.1 -* name (string) -* metadata (dict) -* transition_type (string) -* in_offset (TimeRange) -* out_offset (TimeRange) - -### RationalTime.1 -* value (double) -* rate (double) - -### TimeRange.1 -* start_time (RationalTime) -* duration (RationalTime) +To see an autogenerated documentation of the serialized types and their fields, see this: Autogenerated Serialized File Format diff --git a/docs/tutorials/otio-serialized-schema-only-fields.md b/docs/tutorials/otio-serialized-schema-only-fields.md new file mode 100644 index 000000000..9efda3749 --- /dev/null +++ b/docs/tutorials/otio-serialized-schema-only-fields.md @@ -0,0 +1,249 @@ +# OpenTimelineIO Serialized Data Documentation + +This document is a list of all the OpenTimelineIO classes that serialize to and +from JSON, omitting plugins classes and docstrings. + +This document is automatically generated by running + docs/autogen_serialized_datamodel.py, or by running `make doc-model`. It is + part of the unit tests suite and should be updated whenever the schema changes. + If it needs to be updated, run: `make doc-model-update` and this file should be + regenerated. + +# Classes + + +## Module: opentimelineio.adapters + +### Adapter.1 + +parameters: +- *execution_scope* +- *filepath* +- *name* +- *suffixes* + +## Module: opentimelineio.core + +### Composable.1 + +parameters: +- *metadata* +- *name* + +### Composition.1 + +parameters: +- *children* +- *effects* +- *markers* +- *metadata* +- *name* +- *source_range* + +### Item.1 + +parameters: +- *effects* +- *markers* +- *metadata* +- *name* +- *source_range* + +### MediaReference.1 + +parameters: +- *available_range* +- *metadata* +- *name* + +## Module: opentimelineio.hooks + +### HookScript.1 + +parameters: +- *execution_scope* +- *filepath* +- *name* + +## Module: opentimelineio.media_linker + +### MediaLinker.1 + +parameters: +- *execution_scope* +- *filepath* +- *name* + +## Module: opentimelineio.opentime + +### RationalTime.1 + +parameters: +- *rate* +- *value* + +### TimeRange.1 + +parameters: +- *duration* +- *start_time* + +### TimeTransform.1 + +parameters: +- *offset* +- *rate* +- *scale* + +## Module: opentimelineio.plugins + +### PluginManifest.1 + +parameters: +- *adapters* +- *hook_scripts* +- *hooks* +- *media_linkers* +- *schemadefs* + +### PythonPlugin.1 + +parameters: +- *execution_scope* +- *filepath* +- *name* + +## Module: opentimelineio.schema + +### Clip.1 + +parameters: +- *effects* +- *markers* +- *media_reference* +- *metadata* +- *name* +- *source_range* + +### Effect.1 + +parameters: +- *effect_name* +- *metadata* +- *name* + +### FreezeFrame.1 + +parameters: +- *effect_name* +- *metadata* +- *name* +- *time_scalar* + +### LinearTimeWarp.1 + +parameters: +- *effect_name* +- *metadata* +- *name* +- *time_scalar* + +### TimeEffect.1 + +parameters: +- *effect_name* +- *metadata* +- *name* + +### ExternalReference.1 + +parameters: +- *available_range* +- *metadata* +- *name* +- *target_url* + +### Gap.1 + +parameters: +- *effects* +- *markers* +- *metadata* +- *name* +- *source_range* + +### GeneratorReference.1 + +parameters: +- *available_range* +- *generator_kind* +- *metadata* +- *name* +- *parameters* + +### Marker.2 + +parameters: +- *color* +- *marked_range* +- *metadata* +- *name* + +### MissingReference.1 + +parameters: +- *available_range* +- *metadata* +- *name* + +### SchemaDef.1 + +parameters: +- *execution_scope* +- *filepath* +- *name* + +### SerializableCollection.1 + +parameters: +- *children* +- *metadata* +- *name* + +### Stack.1 + +parameters: +- *children* +- *effects* +- *markers* +- *metadata* +- *name* +- *source_range* + +### Timeline.1 + +parameters: +- *global_start_time* +- *metadata* +- *name* +- *tracks* + +### Track.1 + +parameters: +- *children* +- *effects* +- *kind* +- *markers* +- *metadata* +- *name* +- *source_range* + +### Transition.1 + +parameters: +- *in_offset* +- *metadata* +- *name* +- *out_offset* +- *transition_type* diff --git a/docs/tutorials/otio-serialized-schema.md b/docs/tutorials/otio-serialized-schema.md new file mode 100644 index 000000000..b162789bb --- /dev/null +++ b/docs/tutorials/otio-serialized-schema.md @@ -0,0 +1,548 @@ +# OpenTimelineIO Serialized Data Documentation + +This document is a list of all the OpenTimelineIO classes that serialize to and +from JSON, omitting SchemaDef plugins. + +This document is automatically generated by running + docs/autogen_serialized_datamodel.py, or by running `make doc-model`. It is + part of the unit tests suite and should be updated whenever the schema changes. + If it needs to be updated, run: `make doc-model-update` and this file should be + regenerated. + +# Classes + + +## Module: opentimelineio.adapters + +### Adapter.1 + +*full module path*: `opentimelineio.adapters.adapter.Adapter` + +*documentation*: + +``` +Adapters convert between OTIO and other formats. + + Note that this class is not subclassed by adapters. Rather, an adapter is + a python module that implements at least one of the following functions: + + write_to_string(input_otio) + write_to_file(input_otio, filepath) (optionally inferred) + read_from_string(input_str) + read_from_file(filepath) (optionally inferred) + + ...as well as a small json file that advertises the features of the adapter + to OTIO. This class serves as the wrapper around these modules internal + to OTIO. You should not need to extend this class to create new adapters + for OTIO. + + For more information: + https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an-adapter.html# # noqa + +``` + +parameters: +- *execution_scope*: Describes whether this adapter is executed in the current python process or in a subshell. Options are: ['in process', 'out of process']. +- *filepath*: Absolute path or relative path to adapter module from location of json. +- *name*: Adapter name. +- *suffixes*: File suffixes associated with this adapter. + +## Module: opentimelineio.core + +### Composable.1 + +*full module path*: `opentimelineio.core.composable.Composable` + +*documentation*: + +``` +An object that can be composed by tracks. + + Base class of: + Item + Transition + +``` + +parameters: +- *metadata*: Metadata dictionary for this Composable. +- *name*: Composable name. + +### Composition.1 + +*full module path*: `opentimelineio.core.composition.Composition` + +*documentation*: + +``` +Base class for an OTIO Item that contains other Items. + + Should be subclassed (for example by Track and Stack), not used + directly. + +``` + +parameters: +- *children*: Items contained by this composition. +- *effects*: List of effects on this item. +- *markers*: List of markers on this item. +- *metadata*: Metadata dictionary for this item. +- *name*: Item name. +- *source_range*: Range of source to trim to. Can be None or a TimeRange. + +### Item.1 + +*full module path*: `opentimelineio.core.item.Item` + +*documentation*: + +``` +An Item is a Composable that can be part of a Composition or Timeline. + + More specifically, it is a Composable that has meaningful duration. + + Can also hold effects and markers. + + Base class of: + - Composition (and children) + - Clip + - Gap + +``` + +parameters: +- *effects*: List of effects on this item. +- *markers*: List of markers on this item. +- *metadata*: Metadata dictionary for this item. +- *name*: Item name. +- *source_range*: Range of source to trim to. Can be None or a TimeRange. + +### MediaReference.1 + +*full module path*: `opentimelineio.core.media_reference.MediaReference` + +*documentation*: + +``` +Base Media Reference Class. + + Currently handles string printing the child classes, which expose interface + into its data dictionary. + + The requirement is that the schema is named so that external systems can + fetch the required information correctly. + +``` + +parameters: +- *available_range*: Available range of media in this media reference. +- *metadata*: Metadata dictionary. +- *name*: Name of this media reference. + +## Module: opentimelineio.hooks + +### HookScript.1 + +*full module path*: `opentimelineio.hooks.HookScript` + +*documentation*: + +``` +None +``` + +parameters: +- *execution_scope*: Describes whether this adapter is executed in the current python process or in a subshell. Options are: ['in process', 'out of process']. +- *filepath*: Absolute path or relative path to adapter module from location of json. +- *name*: Adapter name. + +## Module: opentimelineio.media_linker + +### MediaLinker.1 + +*full module path*: `opentimelineio.media_linker.MediaLinker` + +*documentation*: + +``` +None +``` + +parameters: +- *execution_scope*: Describes whether this adapter is executed in the current python process or in a subshell. Options are: ['in process', 'out of process']. +- *filepath*: Absolute path or relative path to adapter module from location of json. +- *name*: Adapter name. + +## Module: opentimelineio.opentime + +### RationalTime.1 + +*full module path*: `opentimelineio.opentime.RationalTime` + +*documentation*: + +``` + Represents an instantaneous point in time, value * (1/rate) seconds + from time 0seconds. + +``` + +parameters: +- *rate*: None +- *value*: None + +### TimeRange.1 + +*full module path*: `opentimelineio.opentime.TimeRange` + +*documentation*: + +``` +Contains a range of time, starting (and including) start_time and + lasting duration.value * (1/duration.rate) seconds. + + A 0 duration TimeRange is the same as a RationalTime, and contains only the + start_time of the TimeRange. + +``` + +parameters: +- *duration*: None +- *start_time*: None + +### TimeTransform.1 + +*full module path*: `opentimelineio.opentime.TimeTransform` + +*documentation*: + +``` +1D Transform for RationalTime. Has offset and scale. +``` + +parameters: +- *offset*: Represents an instantaneous point in time, value * (1/rate) seconds +from time 0seconds. +- *rate*: None +- *scale*: float(x) -> floating point number + +Convert a string or number to a floating point number, if possible. + +## Module: opentimelineio.plugins + +### PluginManifest.1 + +*full module path*: `opentimelineio.plugins.manifest.Manifest` + +*documentation*: + +``` +Defines an OTIO plugin Manifest. + + This is an internal OTIO implementation detail. A manifest tracks a + collection of adapters and allows finding specific adapters by suffix + + For writing your own adapters, consult: + https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an-adapter.html# + +``` + +parameters: +- *adapters*: Adapters this manifest describes. +- *hook_scripts*: Scripts that can be attached to hooks. +- *hooks*: Hooks that hooks scripts can be attached to. +- *media_linkers*: Media Linkers this manifest describes. +- *schemadefs*: Schemadefs this manifest describes. + +### PythonPlugin.1 + +*full module path*: `opentimelineio.plugins.python_plugin.PythonPlugin` + +*documentation*: + +``` +A class of plugin that is encoded in a python module, exposed via a + manifest. + +``` + +parameters: +- *execution_scope*: Describes whether this adapter is executed in the current python process or in a subshell. Options are: ['in process', 'out of process']. +- *filepath*: Absolute path or relative path to adapter module from location of json. +- *name*: Adapter name. + +## Module: opentimelineio.schema + +### Clip.1 + +*full module path*: `opentimelineio.schema.clip.Clip` + +*documentation*: + +``` +The base editable object in OTIO. + + Contains a media reference and a trim on that media reference. + +``` + +parameters: +- *effects*: List of effects on this item. +- *markers*: List of markers on this item. +- *media_reference*: None +- *metadata*: Metadata dictionary for this item. +- *name*: Name of this clip. +- *source_range*: Range of source to trim to. Can be None or a TimeRange. + +### Effect.1 + +*full module path*: `opentimelineio.schema.effect.Effect` + +*documentation*: + +``` +None +``` + +parameters: +- *effect_name*: Name of the kind of effect (example: 'Blur', 'Crop', 'Flip'). +- *metadata*: Metadata dictionary. +- *name*: Name of this effect object. Example: 'BlurByHalfEffect'. + +### FreezeFrame.1 + +*full module path*: `opentimelineio.schema.effect.FreezeFrame` + +*documentation*: + +``` +Hold the first frame of the clip for the duration of the clip. +``` + +parameters: +- *effect_name*: Name of the kind of effect (example: 'Blur', 'Crop', 'Flip'). +- *metadata*: Metadata dictionary. +- *name*: Name of this effect object. Example: 'BlurByHalfEffect'. +- *time_scalar*: Linear time scalar applied to clip. 2.0 = double speed, 0.5 = half speed. + +### LinearTimeWarp.1 + +*full module path*: `opentimelineio.schema.effect.LinearTimeWarp` + +*documentation*: + +``` +A time warp that applies a linear scale across the entire clip +``` + +parameters: +- *effect_name*: Name of the kind of effect (example: 'Blur', 'Crop', 'Flip'). +- *metadata*: Metadata dictionary. +- *name*: Name of this effect object. Example: 'BlurByHalfEffect'. +- *time_scalar*: Linear time scalar applied to clip. 2.0 = double speed, 0.5 = half speed. + +### TimeEffect.1 + +*full module path*: `opentimelineio.schema.effect.TimeEffect` + +*documentation*: + +``` +Base Time Effect Class +``` + +parameters: +- *effect_name*: Name of the kind of effect (example: 'Blur', 'Crop', 'Flip'). +- *metadata*: Metadata dictionary. +- *name*: Name of this effect object. Example: 'BlurByHalfEffect'. + +### ExternalReference.1 + +*full module path*: `opentimelineio.schema.external_reference.ExternalReference` + +*documentation*: + +``` +Reference to media via a url, for example "file:///var/tmp/foo.mov" +``` + +parameters: +- *available_range*: Available range of media in this media reference. +- *metadata*: Metadata dictionary. +- *name*: Name of this media reference. +- *target_url*: URL at which this media lives. For local references, use the 'file://' format. + +### Gap.1 + +*full module path*: `opentimelineio.schema.gap.Gap` + +*documentation*: + +``` +None +``` + +parameters: +- *effects*: List of effects on this item. +- *markers*: List of markers on this item. +- *metadata*: Metadata dictionary for this item. +- *name*: Item name. +- *source_range*: Range of source to trim to. Can be None or a TimeRange. + +### GeneratorReference.1 + +*full module path*: `opentimelineio.schema.generator_reference.GeneratorReference` + +*documentation*: + +``` + + Base class for Generators. + + Generators are media references that become "generators" in editorial + systems. For example, color bars or a solid color. + +``` + +parameters: +- *available_range*: Available range of media in this media reference. +- *generator_kind*: Kind of generator reference, as defined by the schema.generator_reference.GeneratorReferenceTypes enum. +- *metadata*: Metadata dictionary. +- *name*: Name of this media reference. +- *parameters*: Dictionary of parameters for generator. + +### Marker.2 + +*full module path*: `opentimelineio.schema.marker.Marker` + +*documentation*: + +``` + Holds metadata over time on a timeline +``` + +parameters: +- *color*: Color string for this marker (for example: 'RED'), based on the otio.schema.marker.MarkerColor enum. +- *marked_range*: Range this marker applies to, relative to the Item this marker is attached to (e.g. the Clip or Track that owns this marker). +- *metadata*: Metadata dictionary. +- *name*: Name of this marker. + +### MissingReference.1 + +*full module path*: `opentimelineio.schema.missing_reference.MissingReference` + +*documentation*: + +``` +Represents media for which a concrete reference is missing. +``` + +parameters: +- *available_range*: Available range of media in this media reference. +- *metadata*: Metadata dictionary. +- *name*: Name of this media reference. + +### SchemaDef.1 + +*full module path*: `opentimelineio.schema.schemadef.SchemaDef` + +*documentation*: + +``` +None +``` + +parameters: +- *execution_scope*: Describes whether this adapter is executed in the current python process or in a subshell. Options are: ['in process', 'out of process']. +- *filepath*: Absolute path or relative path to adapter module from location of json. +- *name*: Adapter name. + +### SerializableCollection.1 + +*full module path*: `opentimelineio.schema.serializable_collection.SerializableCollection` + +*documentation*: + +``` +A kind of composition which can hold any serializable object. + + This composition approximates the concept of a `bin` - a collection of + SerializableObjects that do not have any compositing meaning, but can + serialize to/from OTIO correctly, with metadata and a named collection. + +``` + +parameters: +- *children*: SerializableObject contained by this container. +- *metadata*: Metadata dictionary for this SerializableCollection. +- *name*: SerializableCollection name. + +### Stack.1 + +*full module path*: `opentimelineio.schema.stack.Stack` + +*documentation*: + +``` +None +``` + +parameters: +- *children*: Items contained by this composition. +- *effects*: List of effects on this item. +- *markers*: List of markers on this item. +- *metadata*: Metadata dictionary for this item. +- *name*: Item name. +- *source_range*: Range of source to trim to. Can be None or a TimeRange. + +### Timeline.1 + +*full module path*: `opentimelineio.schema.timeline.Timeline` + +*documentation*: + +``` +None +``` + +parameters: +- *global_start_time*: Global starting time value and rate of the timeline. +- *metadata*: Metadata dictionary. +- *name*: Name of this timeline. +- *tracks*: Stack of tracks containing items. + +### Track.1 + +*full module path*: `opentimelineio.schema.track.Track` + +*documentation*: + +``` +None +``` + +parameters: +- *children*: Items contained by this composition. +- *effects*: List of effects on this item. +- *kind*: Composition kind (Stack, Track) +- *markers*: List of markers on this item. +- *metadata*: Metadata dictionary for this item. +- *name*: Item name. +- *source_range*: Range of source to trim to. Can be None or a TimeRange. + +### Transition.1 + +*full module path*: `opentimelineio.schema.transition.Transition` + +*documentation*: + +``` +Represents a transition between two items. +``` + +parameters: +- *in_offset*: Amount of the previous clip this transition overlaps, exclusive. +- *metadata*: Metadata dictionary for this Composable. +- *name*: Composable name. +- *out_offset*: Amount of the next clip this transition overlaps, exclusive. +- *transition_type*: Kind of transition, as defined by the schema.transition.TransitionTypes enum. diff --git a/opentimelineio/console/__init__.py b/opentimelineio/console/__init__.py index d5f22cbb0..e5f6e8698 100644 --- a/opentimelineio/console/__init__.py +++ b/opentimelineio/console/__init__.py @@ -35,5 +35,6 @@ otiocat, otiostat, console_utils, + autogen_serialized_datamodel, ) diff --git a/opentimelineio/console/autogen_serialized_datamodel.py b/opentimelineio/console/autogen_serialized_datamodel.py new file mode 100644 index 000000000..a2ce074e1 --- /dev/null +++ b/opentimelineio/console/autogen_serialized_datamodel.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python +# +# Copyright 2019 Pixar Animation Studios +# +# Licensed under the Apache License, Version 2.0 (the "Apache License") +# with the following modification; you may not use this file except in +# compliance with the Apache License and the following modification to it: +# Section 6. Trademarks. is deleted and replaced with: +# +# 6. Trademarks. This License does not grant permission to use the trade +# names, trademarks, service marks, or product names of the Licensor +# and its affiliates, except as required to comply with Section 4(c) of +# the License and to reproduce the content of the NOTICE file. +# +# You may obtain a copy of the Apache License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the Apache License with the above modification is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the Apache License for the specific +# language governing permissions and limitations under the Apache License. +# + + +"""Generates documentation of the serialized data model for OpenTimelineIO.""" + +import argparse +import inspect +import json +import tempfile +import sys + +try: + # python2 + import StringIO as io +except ImportError: + # python3 + import io + +import opentimelineio as otio + + +DOCUMENT_HEADER = """# OpenTimelineIO Serialized Data Documentation + +This document is a list of all the OpenTimelineIO classes that serialize to and +from JSON, omitting SchemaDef plugins. + +This document is automatically generated by running + docs/autogen_serialized_datamodel.py, or by running `make doc-model`. It is + part of the unit tests suite and should be updated whenever the schema changes. + If it needs to be updated, run: `make doc-model-update` and this file should be + regenerated. + +# Classes + +""" + +FIELDS_ONLY_HEADER = """# OpenTimelineIO Serialized Data Documentation + +This document is a list of all the OpenTimelineIO classes that serialize to and +from JSON, omitting plugins classes and docstrings. + +This document is automatically generated by running + docs/autogen_serialized_datamodel.py, or by running `make doc-model`. It is + part of the unit tests suite and should be updated whenever the schema changes. + If it needs to be updated, run: `make doc-model-update` and this file should be + regenerated. + +# Classes + +""" + +CLASS_HEADER_WITH_DOCS = """ +### {classname} + +*full module path*: `{modpath}` + +*documentation*: + +``` +{docstring} +``` + +parameters: +""" + +CLASS_HEADER_ONLY_FIELDS = """ +### {classname} + +parameters: +""" + +MODULE_HEADER = """ +## Module: {modname} +""" + +PROP_HEADER = """- *{propkey}*: {prophelp} +""" + +# @TODO: having type information here would be awesome +PROP_HEADER_NO_HELP = """- *{propkey}* +""" + +# three ways to try and get the property + docstring +PROP_FETCHERS = ( + lambda cl, k: inspect.getdoc(getattr(cl, k)), + lambda cl, k: inspect.getdoc(getattr(cl, "_" + k)), + lambda cl, k: inspect.getdoc(getattr(cl(), k)), +) + + +def _parsed_args(): + """ parse commandline arguments with argparse """ + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + "-d", + "--dryrun", + action="store_true", + default=False, + help="Dryrun mode - print out instead of perform actions" + ) + group.add_argument( + "-o", + "--output", + type=str, + default=None, + help="Update the baseline with the current version" + ) + + return parser.parse_args() + + +# things to skip +SKIP_CLASSES = [otio.core.SerializableObject, otio.core.UnknownSchema] +SKIP_KEYS = ["OTIO_SCHEMA"] # not data, just for the backing format +SKIP_MODULES = ["opentimelineio.schemadef"] # because these are plugins + + +def _generate_model_for_module(mod, classes, modules): + modules.add(mod) + + # fetch the classes from this module + serializeable_classes = [ + thing for thing in mod.__dict__.values() + if ( + inspect.isclass(thing) + and thing not in classes + and issubclass(thing, otio.core.SerializableObject) + or thing in ( + otio.opentime.RationalTime, + otio.opentime.TimeRange, + otio.opentime.TimeTransform, + ) + ) + ] + + # serialize/deserialize the classes to capture their serialized parameters + model = {} + for cl in serializeable_classes: + if cl in SKIP_CLASSES: + continue + + model[cl] = {} + field_dict = json.loads(otio.adapters.otio_json.write_to_string(cl())) + for k in field_dict.keys(): + if k in SKIP_KEYS: + continue + + for fetcher in PROP_FETCHERS: + try: + model[cl][k] = fetcher(cl, k) + break + except AttributeError: + pass + else: + sys.stderr.write("ERROR: could not fetch property: {}".format(k)) + + # Stashing the OTIO_SCHEMA back into the dictionary since the + # documentation uses this information in its header. + model[cl]["OTIO_SCHEMA"] = field_dict["OTIO_SCHEMA"] + + classes.update(model) + + # find new modules to recurse into + new_mods = sorted( + ( + thing for thing in mod.__dict__.values() + if ( + inspect.ismodule(thing) + and thing not in modules + and all(not thing.__name__.startswith(t) for t in SKIP_MODULES) + ) + ), + key=lambda mod: str(mod) + ) + + # recurse into the new modules and update the classes and modules values + [_generate_model_for_module(m, classes, modules) for m in new_mods] + + +def _generate_model(): + classes = {} + modules = set() + _generate_model_for_module(otio, classes, modules) + return classes + + +def _write_documentation(model): + md_with_helpstrings = io.StringIO() + md_only_fields = io.StringIO() + + md_with_helpstrings.write(DOCUMENT_HEADER) + md_only_fields.write(FIELDS_ONLY_HEADER) + + modules = {} + for cl in model: + modules.setdefault(cl.__module__, []).append(cl) + + CURRENT_MODULE = None + for module_list in sorted(modules): + this_mod = ".".join(module_list.split('.')[:2]) + if this_mod != CURRENT_MODULE: + CURRENT_MODULE = this_mod + md_with_helpstrings.write(MODULE_HEADER.format(modname=this_mod)) + md_only_fields.write(MODULE_HEADER.format(modname=this_mod)) + + # because these are classes, they need to sort on their stringified + # names + for cl in sorted(modules[module_list], key=lambda cl: str(cl)): + modname = inspect.getmodule(cl).__name__ + label = model[cl]["OTIO_SCHEMA"] + md_with_helpstrings.write( + CLASS_HEADER_WITH_DOCS.format( + classname=label, + modpath=modname + "." + cl.__name__, + docstring=cl.__doc__ + ) + ) + md_only_fields.write( + CLASS_HEADER_ONLY_FIELDS.format( + classname=label, + ) + ) + + for key, helpstr in sorted(model[cl].items()): + if key in SKIP_KEYS: + continue + md_with_helpstrings.write( + PROP_HEADER.format(propkey=key, prophelp=helpstr) + ) + md_only_fields.write( + PROP_HEADER_NO_HELP.format(propkey=key) + ) + + return md_with_helpstrings.getvalue(), md_only_fields.getvalue() + + +def main(): + """ main entry point """ + args = _parsed_args() + with_docs, without_docs = generate_and_write_documentation() + + # print it out somewhere + if args.dryrun: + print(with_docs) + return + + output = args.output + if not output: + output = tempfile.NamedTemporaryFile( + 'w', + suffix="otio_serialized_schema.md", + delete=False + ).name + + with open(output, 'w') as fo: + fo.write(with_docs) + + # write version without docstrings + prefix, suffix = output.rsplit('.', 1) + output_only_fields = prefix + "-only-fields." + suffix + + with open(output_only_fields, 'w') as fo: + fo.write(without_docs) + + print("wrote documentation to {} and {}".format(output, output_only_fields)) + + +def generate_and_write_documentation(): + model = _generate_model() + return _write_documentation(model) + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index f579e29c7..4909543ad 100755 --- a/setup.py +++ b/setup.py @@ -231,6 +231,7 @@ def test_otio(): 'otiocat = opentimelineio.console.otiocat:main', 'otioconvert = opentimelineio.console.otioconvert:main', 'otiostat = opentimelineio.console.otiostat:main', + 'otioautogen_serialized_schema_docs = opentimelineio.console.autogen_serialized_datamodel:main', ], }, extras_require={ diff --git a/tests/test_serialized_schema.py b/tests/test_serialized_schema.py new file mode 100644 index 000000000..c3f549251 --- /dev/null +++ b/tests/test_serialized_schema.py @@ -0,0 +1,51 @@ +# +# Copyright 2017 Pixar Animation Studios +# +# Licensed under the Apache License, Version 2.0 (the "Apache License") +# with the following modification; you may not use this file except in +# compliance with the Apache License and the following modification to it: +# Section 6. Trademarks. is deleted and replaced with: +# +# 6. Trademarks. This License does not grant permission to use the trade +# names, trademarks, service marks, or product names of the Licensor +# and its affiliates, except as required to comply with Section 4(c) of +# the License and to reproduce the content of the NOTICE file. +# +# You may obtain a copy of the Apache License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the Apache License with the above modification is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the Apache License for the specific +# language governing permissions and limitations under the Apache License. +# + +import unittest +import os + +from opentimelineio.console import autogen_serialized_datamodel as asd + + +class SerializedSchemaTester(unittest.TestCase): + def test_serialized_schema(self): + """Test if the schema has changed since last time the serialized schema + documentation was generated. + """ + + pt = os.path.dirname(os.path.dirname(__file__)) + fp = os.path.join(pt, "docs", "tutorials", "otio-serialized-schema.md") + with open(fp) as fi: + baseline_text = fi.read() + + test_text, _ = asd.generate_and_write_documentation() + + self.maxDiff = None + self.longMessage = True + self.assertMultiLineEqual( + baseline_text, + test_text, + "\n The schema has changed and the autogenerated documentation in {}" + " needs to be updated. run: `make doc-model-update`".format(fp) + )