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