|
| 1 | +# DR002 Versioning Traits and Specification - generated view classes |
| 2 | + |
| 3 | +- **Status:** Decided |
| 4 | +- **Impact:** High |
| 5 | +- **Driver:** @feltech |
| 6 | +- **Approver:** @elliotcmorris @themissingcow |
| 7 | +- **Outcome:** Traits and specifications will be versioned independent |
| 8 | + of the schema, there will be no concept of a schema version, and |
| 9 | + Trait/Specification view classes will be generated with version |
| 10 | + suffixes on the class name. |
| 11 | + |
| 12 | +## Background |
| 13 | + |
| 14 | +The medium of data exchange between a host and a manager is a logically |
| 15 | +opaque data blob, i.e. a `TraitsData` object. In order to add/extract |
| 16 | +information to/from this object, Trait and/or Specification view classes |
| 17 | +must be used[^1]. These classes wrap a `TraitsData` instance, and |
| 18 | +provide a suite of accessor and mutator methods that are relevant to the |
| 19 | +target trait. The classes are generated from a YAML schema (e.g. see |
| 20 | +[traits.yml](../traits.yml)). |
| 21 | + |
| 22 | +Hosts and managers may use different versions of the schema, and hence |
| 23 | +different versions of the view classes, and yet still wish to work |
| 24 | +together. |
| 25 | + |
| 26 | +This decision record follows on from a previous decision (OpenAssetIO |
| 27 | +[DR023](https://github.com/OpenAssetIO/OpenAssetIO/blob/main/doc/decisions/DR023-Versioning-traits-and-specifications-method.md)) |
| 28 | +that communicating a trait's version should be done by bundling the |
| 29 | +version number with the data blob that is communicated across the API, |
| 30 | +i.e. within `TraitsData`, most likely by appending the version number to |
| 31 | +the unique trait ID. |
| 32 | + |
| 33 | +With this previous decision in mind, we then need to decide on how the |
| 34 | +trait versions are represented in the high level interface, i.e. in |
| 35 | +the definition and usage of Trait/Specification view classes. |
| 36 | + |
| 37 | +A motivating example should make this problem clear. |
| 38 | + |
| 39 | +[^1]: In reality, a `TraitsData` is a simple dictionary-like structure, |
| 40 | +and the `TraitsData` type has a low-level interface for interacting with |
| 41 | +it, but usage of this is discouraged. |
| 42 | + |
| 43 | +### Motivating example |
| 44 | + |
| 45 | +An example usage of the current form of these generated classes might |
| 46 | +be: |
| 47 | + |
| 48 | +```python |
| 49 | +url = LocatableContentTrait(trait_data).getLocation() |
| 50 | +``` |
| 51 | + |
| 52 | +Imagine that we want to rename the LocatableContent trait's `"location"` |
| 53 | +property to a more descriptive `"url"` property, hence changing the |
| 54 | +generated view class's method from `getLocation` to `getUrl`. |
| 55 | + |
| 56 | +Given that hosts and managers are developed independently, we may end up |
| 57 | +with a situation where one side is setting `"location"` (using |
| 58 | +`setLocation`) in the data, handing it over to the other side, who then |
| 59 | +attempts to read `"url"` (using `getUrl`). I.e. we have a version |
| 60 | +mismatch. |
| 61 | + |
| 62 | +There is therefore an incompatibility at the data layer (i.e. field |
| 63 | +names differ for the same semantic information). With C++, the data |
| 64 | +layer is where the incompatibility ends. The Trait/Specification view |
| 65 | +classes are private utility classes whose symbols should not be |
| 66 | +exported, so there will be no source or binary incompatibility. |
| 67 | + |
| 68 | +However, with Python there is no such concept of a private, build-time |
| 69 | +only, class. The manager plugin and host application must use the same |
| 70 | +`openassetio-mediacreation` distribution package in the Python |
| 71 | +environment (not considering, for the moment, custom vendoring). So one |
| 72 | +side or the other will hit an `AttributeError` exception when trying to |
| 73 | +use a method from the version they developed against, rather than the |
| 74 | +version installed into the environment. |
| 75 | + |
| 76 | +In order to interoperate, previous versions of Trait/Specification view |
| 77 | +classes must be made available, so that fallback behaviour can be coded. |
| 78 | +In this example, the side that attempts to use the newer `getUrl` to |
| 79 | +read the data must be able to detect that it won't work and fall back to |
| 80 | +the previous version's `getLocation`. |
| 81 | + |
| 82 | +### Assumptions |
| 83 | + |
| 84 | +* A Trait/Specification view class is needed for each version, such |
| 85 | + that a user can imbue a particular version of a trait in some data; |
| 86 | + and can detect that a particular version of a trait is imbued in some |
| 87 | + data. |
| 88 | +* Trait unique IDs will be suffixed with a version number. This means |
| 89 | + two Trait view classes for the same trait, but for different versions, |
| 90 | + will be treated as if they are entirely separate traits. |
| 91 | + Version-agnostic utility functions may be added in the future, but it |
| 92 | + is out of scope for now. |
| 93 | +* If a Specification view class is used to construct/imbue a trait |
| 94 | + set/data, that data will _not_ have the Specification version encoded |
| 95 | + in the data directly (only implicitly through the versioned IDs of the |
| 96 | + composite traits). |
| 97 | + |
| 98 | +## Relevant data |
| 99 | + |
| 100 | +[OpenTimelineIO schema |
| 101 | +versioning](https://opentimelineio.readthedocs.io/en/latest/tutorials/otio-file-format-specification.html#example) |
| 102 | +is perhaps the closest analog. The version of the schema is appended to |
| 103 | +the schema ID whenever it appears within a OTIO JSON document. |
| 104 | + |
| 105 | +The options presented were arrived at by sketching a proposal in [a Pull |
| 106 | +Request](https://github.com/OpenAssetIO/OpenAssetIO-MediaCreation/pull/90), |
| 107 | +soliciting feedback, and iterating. The final form of that PR reflects |
| 108 | +the chosen option. |
| 109 | + |
| 110 | +## Options considered |
| 111 | + |
| 112 | +### Option 1 - Per schema versioning |
| 113 | + |
| 114 | +When traits or specifications in the YAML document are updated, a |
| 115 | +top-level schema version is incremented. During codegen, top-level |
| 116 | +namespaces are created by providing multiple YAML documents, one for |
| 117 | +each schema version. |
| 118 | + |
| 119 | +For example |
| 120 | + |
| 121 | +```python |
| 122 | +from openassetio_mediacreation.v1.traits.content import LocatableContent as LocatableContent_v1 |
| 123 | +from openassetio_mediacreation.v2.traits.content import LocatableContent as LocatableContent_v2 |
| 124 | +from openassetio_mediacreation.v2.specifications.twoDimensional import ImageSpecification |
| 125 | +``` |
| 126 | + |
| 127 | +#### Pros |
| 128 | + |
| 129 | +- Tantalising possibility to use [Python namespace |
| 130 | + packages](https://packaging.python.org/en/latest/guides/packaging-namespace-packages) |
| 131 | + to allow different schema versions to be installed independently |
| 132 | + side-by-side. |
| 133 | +- The schema version a specification comes from instantly tells you the |
| 134 | + schema version of the constituent traits. |
| 135 | +- The YAML is kept small and focussed just on the latest versions. |
| 136 | +- Minimal changes to the `traitgen` tool and existing YAML documents. |
| 137 | +- Maintaining only the latest versions in the live YAML document |
| 138 | + prevents accidental changes to old versions that could break backward |
| 139 | + compatibility. |
| 140 | +- The consuming build system is in charge of deciding which schema |
| 141 | + versions are available for code to use at build/run time. |
| 142 | + I.e. a host/manager only need generate the subpackages they support. |
| 143 | +- Once it is clear that a host/manager understands a particular schema |
| 144 | + version (via `managementPolicy` or otherwise), the communicating |
| 145 | + manager/host can be confident in using that schema version for other |
| 146 | + traits/specifications. |
| 147 | + |
| 148 | +#### Cons |
| 149 | + |
| 150 | +- A source-incompatible breaking change, unless significant |
| 151 | + special-casing is added. |
| 152 | +- Verbose when using two versions in the same source file, either |
| 153 | + requiring use of qualified names (e.g. `v1.traits.LocatableContent`) |
| 154 | + or additional aliasing (e.g. |
| 155 | + `from ... import LocatableContent as LocatableContent_v1`). |
| 156 | +- Not clear at-a-glance which traits have changed between schema |
| 157 | + versions, e.g. it's not clear if |
| 158 | + `v2.traits.content.LocatableContentTrait` is the same as |
| 159 | + `v1.traits.content.LocatableContentTrait`. |
| 160 | +- Must compare multiple YAML documents side-by-side in order to discover |
| 161 | + the history of changes to a particular trait/specification. |
| 162 | +- Traits/specifications that are unchanged between versions implies |
| 163 | + duplicated code across namespaces (though likely simply aliased). |
| 164 | +- Independently generated/installed subpackages for each schema version |
| 165 | + would mean that deprecation warnings could not be added to old |
| 166 | + versions. This is mitigated if multiple versions are generated |
| 167 | + together, where the older version can be detected and deprecation |
| 168 | + warnings added by codegen. |
| 169 | + |
| 170 | +### Option 2 - Per Trait/Specification versioning |
| 171 | + |
| 172 | +A single YAML document is maintained, where each trait/specification |
| 173 | +definition branches off into a list of versions. Old |
| 174 | +trait/specification versions can be marked as deprecated and removed |
| 175 | +eventually, to prevent infinite growth. |
| 176 | + |
| 177 | +For example |
| 178 | + |
| 179 | +```python |
| 180 | +from openassetio_mediacreation.traits.content import LocatableContent_v1 |
| 181 | +from openassetio_mediacreation.traits.content import LocatableContent_v2 |
| 182 | +from openassetio_mediacreation.specifications.twoDimensional import ImageSpecification_v2 |
| 183 | +``` |
| 184 | + |
| 185 | +#### Pros |
| 186 | + |
| 187 | +- Fairly trivial to say that the first version "`_v1`" is equivalent to |
| 188 | + "" (blank), and to ensure that v1's trait ID doesn't contain a version |
| 189 | + tag, then e.g. the `LocatableContent` class continues to work as |
| 190 | + before versioning was introduced, making this option fully source |
| 191 | + compatible with legacy code. I.e. not a breaking change. |
| 192 | +- Placing versions alongside one-another in the YAML definition allows |
| 193 | + easy discovery of the history of changes. |
| 194 | +- IDE code completion will list all versions of a Trait/Specification |
| 195 | + view class next to one-another. |
| 196 | +- More natural for hosts and managers to do targeted compatibility |
| 197 | + around specific traits. |
| 198 | + |
| 199 | +#### Cons |
| 200 | + |
| 201 | +- No indication of the version of the constituent traits from the |
| 202 | + version of a Specification view class. |
| 203 | +- Large change to `traitgen` tool and non-trivial breaking change to |
| 204 | + YAML documents. |
| 205 | +- Keeping old versions in a living document (as opposed to e.g. git |
| 206 | + history) is a potential source of accidental breakages to backward |
| 207 | + compatibility. |
| 208 | +- Generating all possible versions bloats an application's distribution, |
| 209 | + when it may only use a small subset of them. Configuring which |
| 210 | + versions to generate would mean maintaining a long list of options, |
| 211 | + one per trait. |
| 212 | +- Higher level branching on a schema version is never possible. |
| 213 | +- A specification's version must be bumped when a constituent trait has |
| 214 | + a version bump, even if nothing else in the specification has changed. |
| 215 | + Conceptually, specifications are trait version agnostic, but must |
| 216 | + become version-aware for the purposes of codegen, which is |
| 217 | + inconsistent. |
| 218 | + |
| 219 | +## Outcome |
| 220 | + |
| 221 | +We will implement Option 2 - Per Trait/Specification versioning. |
| 222 | + |
| 223 | +A huge benefit is how much easier it is to make this solution a |
| 224 | +non-breaking change to current users. |
| 225 | + |
| 226 | +In addition, it has better discoverability through IDE code completion, |
| 227 | +and it is easier to view history through a single YAML document rather |
| 228 | +than across several documents. |
| 229 | + |
| 230 | +There will be a rather large change to the `traitgen` tool and the YAML |
| 231 | +JSON schema, causing a headache for any early adopters who are |
| 232 | +generating their own traits. However, this is less critical than changes |
| 233 | +to the generated output in use within pipelines. |
0 commit comments