Repository: OpenAssetIO/OpenAssetIO (language: python)

Issue:
Relax `TraitsData::getTraitProperty` exceptions
## What

Return `false` instead of throwing if the requested trait is missing.

## Why

There are many host situations where they wish to use a default value when attempting to check for a trait property. Splitting the failure mode between missing traits and missing properties makes for cumbersome host code. It forces a host to always check if a trait is imbued first, before then potentially looking up the value. Traits being missing isn't any more exceptional than their properties in many real-world applications, forcing an `isImbued` check makes code more complex and potentially doubles the number of map lookups for that trait.

If a host wants to explicitly differentiate the two failure modes, then the current approach can still be used.

eg:
```python
data = resolve(..., {LocatableContentTrait.kId}, ...)
if LocatableContentTrait.isImbuedTo(data):
  value = LocatableContentTrait(data).getLocation(someDefault)
```
Becomes

```python
data = resolve(...)
value = LocatableContentTrait(data).getLocation(someDefault)
```

## Acceptance Criteria

- `getTraitProperty` returns `false` if the trait is not imbued _or_ the property is not set.


Discussion / hints:
NB; TraitGen is missing some tests for the Python code that asserts the behaviours when the traitsdata is empty, 

Fix patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index cc0a36194..1a4f3b2c4 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,6 +1,18 @@
 Release Notes
 =============
 
+v1.0.0-alpha.xx
+---------------
+
+### Breaking changes
+
+- Attempting to retrieve a trait property with
+  `TraitsData.getTraitProperty` or using trait view classes no longer
+  raises an exception if the trait itself is missing. This simplifies
+  many common access patterns, and can remove the need for
+  `hasTrait`/`isImbuedTo` checks.
+  [#970](https://github.com/OpenAssetIO/OpenAssetIO/issues/970)
+
 v1.0.0-alpha.14
 ---------------
 
diff --git a/src/openassetio-core/src/TraitsData.cpp b/src/openassetio-core/src/TraitsData.cpp
index 71d8f8ab7..a9b7b8a80 100644
--- a/src/openassetio-core/src/TraitsData.cpp
+++ b/src/openassetio-core/src/TraitsData.cpp
@@ -41,14 +41,16 @@ class TraitsData::Impl {
   // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
   bool getTraitProperty(trait::property::Value* out, const trait::TraitId& traitId,
                         const trait::property::Key& propertyKey) const {
-    // Use `at` deliberately to trigger exception if trait doesn't exist
-    const auto& traitDict = data_.at(traitId);
-
-    const auto& iter = traitDict.find(propertyKey);
-    if (iter == traitDict.end()) {
+    const auto& traitIter = data_.find(traitId);
+    if (traitIter == data_.end()) {
+      return false;
+    }
+    const auto& traitDict = traitIter->second;
+    const auto& propertyIter = traitDict.find(propertyKey);
+    if (propertyIter == traitDict.end()) {
       return false;
     }
-    *out = iter->second;
+    *out = propertyIter->second;
     return true;
   }
 


Test patch:
diff --git a/src/openassetio-python/tests/package/test_traitsdata.py b/src/openassetio-python/tests/package/test_traitsdata.py
index 8461ee125..c92312c05 100644
--- a/src/openassetio-python/tests/package/test_traitsdata.py
+++ b/src/openassetio-python/tests/package/test_traitsdata.py
@@ -123,9 +123,8 @@ def test_when_trait_is_not_found_then_set_adds_trait(self, a_traitsdata):
         assert new_trait_id in a_traitsdata.traitSet()
         assert a_traitsdata.getTraitProperty(new_trait_id, a_property) == a_value
 
-    def test_when_trait_is_not_found_then_get_raises_IndexError(self, a_traitsdata):
-        with pytest.raises(IndexError):
-            _ = a_traitsdata.getTraitProperty("unknown_trait", "a string")
+    def test_when_trait_is_not_found_then_get_returns_None(self, a_traitsdata):
+        assert a_traitsdata.getTraitProperty("unknown_trait", "a string") is None
 
     def test_when_value_is_not_supported_then_set_raises_TypeError(self, a_traitsdata):
         with pytest.raises(TypeError):