Skip to content

Commit 71682e9

Browse files
committed
[Docs] Add DR002 for versioning in codegen
Part of OpenAssetIO#88. Consolidate the discussion, provoked by iterations of the design proposal in OpenAssetIO#90, into a decision record. Signed-off-by: David Feltell <[email protected]>
1 parent f20a2c4 commit 71682e9

File tree

1 file changed

+233
-0
lines changed

1 file changed

+233
-0
lines changed
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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

Comments
 (0)