Skip to content

Commit 9f0667f

Browse files
authored
Merge pull request #113 from feltech/work/88-traitVersioning
Add trait versioning
2 parents 1cd194a + aa5abef commit 9f0667f

26 files changed

+1733
-502
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ ij_visual_guides = 72
1616
[*.json]
1717
indent_size = 2
1818

19-
[*.yml]
19+
[*.y*ml]
2020
indent_size = 2
2121

2222
[*.md]

RELEASE_NOTES.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
11
Release Notes
22
=============
33

4+
v1.0.0-alpha.x
5+
--------------
6+
7+
### Breaking changes
8+
9+
- Updated the YAML schema to group traits/specifications under a version
10+
number key.
11+
[#80](https://github.com/OpenAssetIO/OpenAssetIO-TraitGen/issues/80)
12+
13+
### New features
14+
15+
- Added support for trait versioning. Suffixed generated
16+
trait/specification view classes with a `_vX` and trait IDs with a `.vX`
17+
(where `X` is a version number), except for the first version of a
18+
trait, where the ID has no version suffix, retaining backward
19+
compatibility.
20+
[#80](https://github.com/OpenAssetIO/OpenAssetIO-TraitGen/issues/80)
21+
22+
- Added an optional `deprecated` field to trait and specification YAML
23+
definitions, which causes a deprecation warning/annotation to be
24+
generated for all versions of that trait/specification.
25+
[#80](https://github.com/OpenAssetIO/OpenAssetIO-TraitGen/issues/80)
26+
27+
### Improvements
28+
29+
- Updated classes without a version suffix to alias version 1, but with
30+
a deprecation warning/annotation, for backward compatibility.
31+
[#80](https://github.com/OpenAssetIO/OpenAssetIO-TraitGen/issues/80)
32+
33+
434
v1.0.0-alpha.12
535
--------------
636

python/openassetio_traitgen/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,10 @@ def _log_package_declaration(package, logger):
176176
for namespace in package.traits:
177177
logger.info(f"{namespace.id}:")
178178
for trait in namespace.members:
179-
logger.info(" - %s", trait.name)
179+
logger.info(" - %s (v%s)", trait.name, trait.version)
180180
if package.specifications:
181181
logger.info("Specifications:")
182182
for namespace in package.specifications:
183183
logger.info(f"{namespace.id}:")
184184
for specification in namespace.members:
185-
logger.info(" - %s", specification.id)
185+
logger.info(" - %s (v%s)", specification.id, specification.version)

python/openassetio_traitgen/datamodel.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ class TraitDeclaration(NamedTuple):
8282
# A short name for the Trait that is only unique within its
8383
# namespace.
8484
name: str
85+
# Whether this trait is deprecated.
86+
deprecated: bool
87+
# Version of the trait.
88+
version: str
8589
# A user-facing description of the Trait and its purpose.
8690
description: str
8791
# User-facing hints as to the usage of this trait, in relation to
@@ -115,14 +119,16 @@ class TraitReference(NamedTuple):
115119
namespace: str
116120
# The package the trait belongs to
117121
package: str
122+
# Version of the trait
123+
version: str
118124
# The shortest list of elements from package, namespace and name
119125
# that is required to form a unique name for this trait
120126
# relative to the specification. These should be used
121127
# when building accessor method names to retrieve a
122128
# Trait instance from a Specification, as it handles the
123129
# case where a Specification may reference two identically
124130
# named traits in different packages or namespaces.
125-
unique_name_parts: Tuple[str]
131+
unique_name_parts: Tuple[str, ...]
126132

127133

128134
class SpecificationDeclaration(NamedTuple):
@@ -132,6 +138,10 @@ class SpecificationDeclaration(NamedTuple):
132138

133139
# The unique name of Specification within its namespace.
134140
id: str
141+
# Whether this specification is deprecated.
142+
deprecated: bool
143+
# Version of the specification.
144+
version: str
135145
# A user-facing description of the Specification and its purpose.
136146
description: str
137147
# User-facing hints as to the usage of this trait, in relation to

python/openassetio_traitgen/generators/cpp.py

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
A traitgen generator that outputs a C++ package based on the
1818
openassetio_traitgen PackageDefinition model.
1919
"""
20+
import collections
21+
import itertools
22+
2023
# TODO(DF): Refactor to pull out common code, then remove this
2124
# suppression.
2225
# pylint: disable=duplicate-code
@@ -180,12 +183,27 @@ def __render_namespace(
180183
)
181184
imports = []
182185

183-
# Render a file per class (trait or specification).
184-
for declaration in namespace.members:
186+
# We group multiple versions of the same trait (or
187+
# specification) together to render in the same header. Note
188+
# that declarations in a namespace are already sorted
189+
# appropriately by name, so we don't need to sort before
190+
# applying groupby.
191+
declarations_by_name = itertools.groupby(
192+
namespace.members,
193+
lambda declaration: declaration.name if kind == "traits" else declaration.id,
194+
)
195+
196+
# Render a file per trait/specification, containing all
197+
# versions of that trait/specification.
198+
for name, declarations in declarations_by_name:
185199
if kind == "traits":
186-
file_name = self.__render_trait(namespace, declaration, namespace_abs_path)
200+
file_name = self.__render_trait(
201+
namespace, name, tuple(declarations), namespace_abs_path
202+
)
187203
else:
188-
file_name = self.__render_specification(namespace, declaration, namespace_abs_path)
204+
file_name = self.__render_specification(
205+
namespace, name, tuple(declarations), namespace_abs_path
206+
)
189207

190208
imports.append(f"{namespace_name}/{file_name}")
191209

@@ -207,7 +225,8 @@ def __render_namespace(
207225
def __render_trait(
208226
self,
209227
namespace: NamespaceDeclaration,
210-
declaration: TraitDeclaration,
228+
name: str,
229+
declarations: tuple[TraitDeclaration, ...],
211230
namespace_abs_path: str,
212231
) -> str:
213232
"""
@@ -216,24 +235,25 @@ def __render_trait(
216235
Creates a single header file containing a single trait view
217236
class.
218237
"""
219-
cls_name = self.__env.filters["to_cpp_class_name"](declaration.name) + "Trait"
238+
header_name = self.__env.filters["to_cpp_class_name"](name) + "Trait"
220239
self.__render_template(
221240
"trait",
222-
os.path.join(namespace_abs_path, f"{cls_name}.hpp"),
241+
os.path.join(namespace_abs_path, f"{header_name}.hpp"),
223242
{
224243
"package": self.__package,
225244
"namespace": namespace,
226-
"trait": declaration,
245+
"versions": declarations,
227246
"openassetio_abi_version": OPENASSETIO_ABI_VERSION,
228247
"traitgen_abi_version": TRAITGEN_ABI_VERSION,
229248
},
230249
)
231-
return f"{cls_name}.hpp"
250+
return f"{header_name}.hpp"
232251

233252
def __render_specification(
234253
self,
235254
namespace: NamespaceDeclaration,
236-
declaration: SpecificationDeclaration,
255+
name: str,
256+
declarations: tuple[SpecificationDeclaration, ...],
237257
namespace_abs_path: str,
238258
) -> str:
239259
"""
@@ -242,19 +262,38 @@ def __render_specification(
242262
Creates a single header file containing a single specification
243263
class.
244264
"""
245-
cls_name = self.__env.filters["to_cpp_class_name"](declaration.id) + "Specification"
265+
266+
# Properties required to interpolate when constructing #include
267+
# directives.
268+
TraitHeaderPathTokens = collections.namedtuple(
269+
"TraitHeaderPathTokens", ("package", "namespace", "name")
270+
)
271+
272+
# All versions of a given trait live in a single header.
273+
# Extract fields required to #include the trait headers
274+
# referenced by all versions of this specification, de-duped.
275+
all_trait_header_path_tokens = sorted(
276+
{
277+
TraitHeaderPathTokens(trait_decl.package, trait_decl.namespace, trait_decl.name)
278+
for spec_decl in declarations
279+
for trait_decl in spec_decl.trait_set
280+
}
281+
)
282+
283+
header_name = self.__env.filters["to_cpp_class_name"](name) + "Specification"
246284
self.__render_template(
247285
"specification",
248-
os.path.join(namespace_abs_path, f"{cls_name}.hpp"),
286+
os.path.join(namespace_abs_path, f"{header_name}.hpp"),
249287
{
250288
"package": self.__package,
251289
"namespace": namespace,
252-
"specification": declaration,
290+
"versions": declarations,
291+
"all_trait_header_path_tokens": all_trait_header_path_tokens,
253292
"openassetio_abi_version": OPENASSETIO_ABI_VERSION,
254293
"traitgen_abi_version": TRAITGEN_ABI_VERSION,
255294
},
256295
)
257-
return f"{cls_name}.hpp"
296+
return f"{header_name}.hpp"
258297

259298
def __render_package_template(
260299
self, package_abs_path: str, name: str, docstring: str, imports: List[str]

python/openassetio_traitgen/generators/python.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def to_py_module_name(string: str):
181181
no_hypens = string.replace("-", "_")
182182
module_name = re.sub(r"[^a-zA-Z0-9_]", "_", no_hypens)
183183
if module_name != no_hypens:
184-
logger.warning(f"Conforming '{string}' to '{module_name}' for module name")
184+
_conform_warning(string, module_name, "module name")
185185
return module_name
186186

187187
def to_py_class_name(string: str):
@@ -190,7 +190,7 @@ def to_py_class_name(string: str):
190190
"""
191191
class_name = helpers.to_upper_camel_alnum(string)
192192
if class_name != string:
193-
logger.warning(f"Conforming '{string}' to '{class_name}' for class name")
193+
_conform_warning(string, class_name, "class name")
194194
validate_identifier(class_name, string)
195195
return class_name
196196

@@ -204,9 +204,7 @@ def to_py_trait_accessor_name(name_parts: List[str]):
204204
accessor_name = helpers.to_lower_camel_alnum(unique_name)
205205
# We expect the first letter to change to lowercase
206206
if accessor_name != f"{unique_name[0].lower()}{unique_name[1:]}":
207-
logger.warning(
208-
f"Conforming '{unique_name}' to '{accessor_name}' for trait getter name"
209-
)
207+
_conform_warning(unique_name, accessor_name, "trait getter name")
210208
validate_identifier(accessor_name, unique_name)
211209
return accessor_name
212210

@@ -218,9 +216,7 @@ def to_py_var_accessor_name(string: str):
218216
"""
219217
accessor_name = helpers.to_upper_camel_alnum(string)
220218
if accessor_name != f"{string[0].upper()}{string[1:]}":
221-
logger.warning(
222-
f"Conforming '{string}' to '{accessor_name}' for property accessor name"
223-
)
219+
_conform_warning(string, accessor_name, "property accessor name")
224220
validate_identifier(accessor_name, string)
225221
return accessor_name
226222

@@ -231,7 +227,7 @@ def to_py_var_name(string: str):
231227
"""
232228
var_name = helpers.to_lower_camel_alnum(string)
233229
if var_name != string:
234-
logger.warning(f"Conforming '{string}' to '{var_name}' for variable name")
230+
_conform_warning(string, var_name, "variable name")
235231
validate_identifier(var_name, string)
236232
return var_name
237233

@@ -251,6 +247,19 @@ def to_py_type(declaration_type):
251247
raise TypeError("Dictionary types are not yet supported as trait properties")
252248
return type_map[declaration_type]
253249

250+
def _conform_warning(original: str, conformed: str, context: str):
251+
"""
252+
Log a warning that an input name has been modified to conform
253+
to a valid identifier, if the warning has not already been
254+
logged.
255+
"""
256+
warning = f"Conforming '{original}' to '{conformed}' for {context}"
257+
if warning in environment.globals["conform_warnings"]:
258+
return
259+
environment.globals["conform_warnings"].append(warning)
260+
logger.warning(warning)
261+
262+
environment.globals["conform_warnings"] = []
254263
environment.filters["to_upper_camel_alnum"] = helpers.to_upper_camel_alnum
255264
environment.filters["to_py_module_name"] = to_py_module_name
256265
environment.filters["to_py_class_name"] = to_py_class_name

python/openassetio_traitgen/parser.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,14 @@ def _unpack_specifications(model: dict, package_id: str) -> List[datamodel.Names
8888
specifications = [
8989
datamodel.SpecificationDeclaration(
9090
id=name,
91+
deprecated=props.get("deprecated", False),
92+
version=version_num,
9193
description=definition.get("description", "").strip(),
9294
trait_set=_unpack_trait_set(definition["traitSet"], package_id),
9395
usage=definition.get("usage", []),
9496
)
95-
for name, definition in data["members"].items()
97+
for name, props in data["members"].items()
98+
for version_num, definition in props["versions"].items()
9699
]
97100
specifications.sort(key=_byId)
98101

@@ -134,8 +137,9 @@ def _unpack_trait_set(trait_set: List[dict], package_id: str) -> List[datamodel.
134137
package = trait.get("package", package_id)
135138
namespace = trait["namespace"]
136139
name = trait["name"]
140+
version = trait["version"]
137141

138-
identifier = _build_trait_id(package, namespace, name)
142+
identifier = _build_trait_id(package, namespace, name, version)
139143

140144
# Check to see which of the possible combinations of reference
141145
# parts is unique for this trait.
@@ -153,6 +157,7 @@ def _unpack_trait_set(trait_set: List[dict], package_id: str) -> List[datamodel.
153157
name=name,
154158
namespace=namespace,
155159
package=package,
160+
version=version,
156161
unique_name_parts=unique_name_parts,
157162
)
158163
)
@@ -162,7 +167,15 @@ def _unpack_trait_set(trait_set: List[dict], package_id: str) -> List[datamodel.
162167
return references
163168

164169

165-
def _build_trait_id(package: str, namespace: str, name: str) -> str:
170+
def _build_trait_id(package: str, namespace: str, name: str, version: str) -> str:
171+
"""
172+
Builds a trait ID from the supplied components.
173+
174+
The first version "1" omits the version suffix to maintain backward
175+
compatibility with existing traits.
176+
"""
177+
if version != "1":
178+
return f"{package}:{namespace}.{name}.v{version}"
166179
return f"{package}:{namespace}.{name}"
167180

168181

@@ -180,13 +193,16 @@ def _unpack_traits(
180193
for namespace, data in model.items():
181194
traits = [
182195
datamodel.TraitDeclaration(
183-
id=_build_trait_id(package_id, namespace, name),
196+
id=_build_trait_id(package_id, namespace, name, version_num),
184197
name=name,
198+
deprecated=props.get("deprecated", False),
199+
version=version_num,
185200
description=definition.get("description", "").strip(),
186201
properties=_unpack_properties(definition.get("properties", {})),
187202
usage=definition.get("usage", []),
188203
)
189-
for name, definition in data["members"].items()
204+
for name, props in data["members"].items()
205+
for version_num, definition in props["versions"].items()
190206
]
191207
traits.sort(key=_byName)
192208

0 commit comments

Comments
 (0)