Skip to content

Commit cc3b921

Browse files
authored
Merge pull request #115 from feltech/work/84-configurableCapabilities
Configurable `hasCapability` responses
2 parents 465795f + 1f65d3d commit cc3b921

7 files changed

+220
-2
lines changed

RELEASE_NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ v1.0.0-alpha.x
66

77
### New features
88

9+
- Added support for configuring the result of `hasCapability(...)`
10+
queries. This allows hosts to test their logic when dealing with
11+
managers that have limited capability.
12+
[#84](https://github.com/OpenAssetIO/OpenAssetIO-Manager-BAL/issues/84)
13+
914
- Added support for `OPENASSETIO_BAL_IDENTIFIER` environment variable,
1015
for overriding the identifier advertised by the BAL plugin/manager.
1116
[#116](https://github.com/OpenAssetIO/OpenAssetIO-Manager-BAL/pull/116)

plugin/openassetio_manager_bal/BasicAssetLibraryInterface.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,26 @@ def settings(self, hostSession):
121121
return self.__settings.copy()
122122

123123
def hasCapability(self, capability):
124+
"""
125+
Override to report either real or configured capabilities.
126+
127+
The default set of capabilities reflect the true capabilities
128+
of BAL.
129+
130+
The reported available capabilities can be configured in the
131+
JSON library using the "capabilities" key, which is a list of
132+
capability name strings, as defined in
133+
`ManagerInterface.kCapabilityNames` - useful for testing host
134+
application logic.
135+
136+
API methods associated with disabled capabilities will
137+
short-circuit and call the base class implementation (which will
138+
raise a `NotImplementedException`).
139+
"""
140+
if self.__library.get("capabilities") is not None:
141+
capabilityStr = self.kCapabilityNames[capability]
142+
return capabilityStr in self.__library["capabilities"]
143+
124144
if capability in (
125145
ManagerInterface.Capability.kEntityReferenceIdentification,
126146
ManagerInterface.Capability.kManagementPolicyQueries,
@@ -215,6 +235,10 @@ def isEntityReferenceString(self, someString, hostSession):
215235

216236
@simulated_delay
217237
def entityExists(self, entityRefs, context, _hostSession, successCallback, errorCallback):
238+
if not self.hasCapability(self.Capability.kExistenceQueries):
239+
super().entityExists(entityRefs, context, _hostSession, successCallback, errorCallback)
240+
return
241+
218242
for idx, ref in enumerate(entityRefs):
219243
try:
220244
# Use resolve-for-read access mode as closest analog.
@@ -262,6 +286,18 @@ def resolve(
262286
errorCallback,
263287
):
264288
# pylint: disable=too-many-locals
289+
if not self.hasCapability(self.Capability.kResolution):
290+
super().resolve(
291+
entityReferences,
292+
traitSet,
293+
access,
294+
context,
295+
hostSession,
296+
successCallback,
297+
errorCallback,
298+
)
299+
return
300+
265301
for idx, ref in enumerate(entityReferences):
266302
try:
267303
entity_info = self.__parse_entity_ref(ref.toString(), access)
@@ -295,6 +331,18 @@ def preflight(
295331
successCallback,
296332
errorCallback,
297333
):
334+
if not self.hasCapability(self.Capability.kPublishing):
335+
super().preflight(
336+
targetEntityRefs,
337+
traitsDatas,
338+
access,
339+
context,
340+
hostSession,
341+
successCallback,
342+
errorCallback,
343+
)
344+
return
345+
298346
if not self.__validate_access(
299347
"preflight",
300348
(PublishingAccess.kWrite,),
@@ -333,6 +381,18 @@ def register(
333381
successCallback,
334382
errorCallback,
335383
):
384+
if not self.hasCapability(self.Capability.kPublishing):
385+
super().register(
386+
targetEntityRefs,
387+
entityTraitsDatas,
388+
access,
389+
context,
390+
hostSession,
391+
successCallback,
392+
errorCallback,
393+
)
394+
return
395+
336396
if not self.__validate_access(
337397
"register",
338398
(PublishingAccess.kWrite,),
@@ -411,6 +471,20 @@ def getWithRelationship(
411471
successCallback,
412472
errorCallback,
413473
):
474+
if not self.hasCapability(self.Capability.kRelationshipQueries):
475+
super().getWithRelationship(
476+
entityReferences,
477+
relationshipTraitsData,
478+
resultTraitSet,
479+
pageSize,
480+
access,
481+
context,
482+
_hostSession,
483+
successCallback,
484+
errorCallback,
485+
)
486+
return
487+
414488
if not self.__validate_access(
415489
"relationship query",
416490
(RelationsAccess.kRead,),
@@ -449,6 +523,20 @@ def getWithRelationships(
449523
successCallback,
450524
errorCallback,
451525
):
526+
if not self.hasCapability(self.Capability.kRelationshipQueries):
527+
super().getWithRelationships(
528+
entityReference,
529+
relationshipTraitsDatas,
530+
resultTraitSet,
531+
pageSize,
532+
access,
533+
context,
534+
_hostSession,
535+
successCallback,
536+
errorCallback,
537+
)
538+
return
539+
452540
if not self.__validate_access(
453541
"relationship query",
454542
(RelationsAccess.kRead,),

schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
"description": "The data store that backs an instance of the BAL manager",
66
"type": "object",
77
"properties": {
8+
"capabilities": {
9+
"description": "List of capabilities that BAL should advertise",
10+
"type": "array",
11+
"items": {
12+
"type": "string",
13+
"description": "The name of a capability, as defined in ManagerInterface.kCapabilityNames"
14+
},
15+
"additionalProperties": false
16+
},
817
"variables": {
918
"description": "Arbitrary variables to be substituted in string trait properties",
1019
"type": "object",

tests/bal_business_logic_suite.py

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from openassetio.access import (
3434
PolicyAccess,
3535
PublishingAccess,
36+
DefaultEntityAccess,
3637
RelationsAccess,
3738
ResolveAccess,
3839
EntityTraitsAccess,
@@ -41,6 +42,7 @@
4142
BatchElementError,
4243
BatchElementException,
4344
ConfigurationException,
45+
NotImplementedException,
4446
)
4547
from openassetio.test.manager.harness import FixtureAugmentedTestCase
4648
from openassetio.trait import TraitsData
@@ -83,8 +85,8 @@ def setUp(self):
8385
"resources",
8486
self._library,
8587
)
86-
self._manager.initialize(new_settings)
8788
self.addCleanup(self.cleanUp)
89+
self._manager.initialize(new_settings)
8890

8991
def cleanUp(self):
9092
self._manager.initialize(self.__old_settings)
@@ -370,7 +372,7 @@ def test_returns_expected_policy_for_managerDriven_for_all_trait_sets(self):
370372
self.assertListEqual(actual, expected)
371373

372374

373-
class Test_hasCapability(FixtureAugmentedTestCase):
375+
class Test_hasCapability_default(FixtureAugmentedTestCase):
374376
"""
375377
Tests that BAL reports expected capabilities
376378
"""
@@ -395,6 +397,96 @@ def test_when_hasCapability_called_on_managerInterface_then_has_mandatory_capabi
395397
)
396398

397399

400+
class Test_hasCapability_override_none(LibraryOverrideTestCase):
401+
_library = "library_business_logic_suite_capabilities_none.json"
402+
403+
def setUp(self):
404+
# Override base class because otherwise it'll raise. The setUp
405+
# in this case _is_ the test.
406+
pass
407+
408+
def test_when_when_library_lists_no_capabilities_then_raises(self):
409+
with self.assertRaises(ConfigurationException) as exc:
410+
# Call base class setup, which will re-initialize the
411+
# manager with the alternative self._library JSON file.
412+
super().setUp()
413+
414+
self.assertEqual(
415+
str(exc.exception),
416+
"Manager implementation for 'org.openassetio.examples.manager.bal' does not"
417+
" support the required capabilities: entityReferenceIdentification,"
418+
" managementPolicyQueries, entityTraitIntrospection",
419+
)
420+
421+
422+
class Test_hasCapability_override_all(LibraryOverrideTestCase):
423+
_library = "library_business_logic_suite_capabilities_all.json"
424+
425+
def test_when_library_lists_all_capabilities_then_hasCapability_is_true_for_all(self):
426+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kStatefulContexts))
427+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kCustomTerminology))
428+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kDefaultEntityReferences))
429+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kResolution))
430+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kPublishing))
431+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kRelationshipQueries))
432+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kExistenceQueries))
433+
434+
435+
class Test_hasCapability_override_minimal(LibraryOverrideTestCase):
436+
_library = "library_business_logic_suite_capabilities_minimal.json"
437+
438+
def test_when_library_lists_minimal_capabilities_then_hasCapability_is_false_for_all(self):
439+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kStatefulContexts))
440+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kCustomTerminology))
441+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kDefaultEntityReferences))
442+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kResolution))
443+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kPublishing))
444+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kRelationshipQueries))
445+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kExistenceQueries))
446+
447+
def test_when_capability_not_supported_then_methods_raise_NotImplementedException(self):
448+
context = self.createTestContext()
449+
450+
with self.assertRaises(NotImplementedException):
451+
self._manager.defaultEntityReference(
452+
[],
453+
DefaultEntityAccess.kRead,
454+
context,
455+
lambda *a: self.fail("Unexpected success"),
456+
lambda *a: self.fail("Unexpected element error"),
457+
)
458+
459+
with self.assertRaises(NotImplementedException):
460+
self._manager.updateTerminology({})
461+
462+
with self.assertRaises(NotImplementedException):
463+
self._manager.resolve([], set(), ResolveAccess.kRead, context)
464+
465+
with self.assertRaises(NotImplementedException):
466+
self._manager.preflight([], [], PublishingAccess.kWrite, context)
467+
468+
with self.assertRaises(NotImplementedException):
469+
self._manager.register([], [], PublishingAccess.kWrite, context)
470+
471+
with self.assertRaises(NotImplementedException):
472+
self._manager.getWithRelationship(
473+
[], TraitsData(), 1, RelationsAccess.kRead, context, set()
474+
)
475+
476+
with self.assertRaises(NotImplementedException):
477+
self._manager.getWithRelationships(
478+
self._manager.createEntityReference("bal:///"),
479+
[],
480+
1,
481+
RelationsAccess.kRead,
482+
context,
483+
set(),
484+
)
485+
486+
with self.assertRaises(NotImplementedException):
487+
self._manager.entityExists([], context)
488+
489+
398490
class Test_entityTraits(FixtureAugmentedTestCase):
399491
def test_when_missing_entity_queried_for_write_then_empty_trait_set_returned(self):
400492
# Missing entities are writable with unrestricted trait set.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"capabilities": [
3+
"entityReferenceIdentification",
4+
"managementPolicyQueries",
5+
"statefulContexts",
6+
"customTerminology",
7+
"resolution",
8+
"publishing",
9+
"relationshipQueries",
10+
"existenceQueries",
11+
"defaultEntityReferences",
12+
"entityTraitIntrospection"
13+
]
14+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"capabilities": [
3+
"entityReferenceIdentification",
4+
"managementPolicyQueries",
5+
"entityTraitIntrospection"
6+
]
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"capabilities": []
3+
}

0 commit comments

Comments
 (0)