Repository: canonical/operator

Issue:
`Framework.ObjectEvents.events` has wrong signature
The signature of the `.events()` method is:

```python
    def events(self) -> Dict[str, EventSource]:
        """Return a mapping of event_kinds to bound_events for [...]
```

so what is it, EventSource or bound_events?
Turns out in practice it is a mapping of str to BoundEvent (docstring is correct, type annotation is wrong).

![image](https://github.com/user-attachments/assets/280a320d-36b2-4718-963f-591b8eafd073)


Discussion:
tonyandrewmeyer: Thanks! Curious that pyright doesn't complain about this.
PietroPasotti: I think it didn't because all attributes that were accessed were coincidentally common between the two types :)
they both talked like ducks

Patch:
diff --git a/ops/framework.py b/ops/framework.py
index 7c96ee5..c47e6b9 100644
--- a/ops/framework.py
+++ b/ops/framework.py
@@ -485,7 +485,7 @@ class ObjectEvents(Object):
                 event_kinds.append(attr_name)
         return event_kinds
 
-    def events(self) -> Dict[str, EventSource]:
+    def events(self) -> Dict[str, BoundEvent]:
         """Return a mapping of event_kinds to bound_events for all available events."""
         return {event_kind: getattr(self, event_kind) for event_kind in self._event_kinds()}
 
diff --git a/ops/main.py b/ops/main.py
index 94105cb..871a749 100644
--- a/ops/main.py
+++ b/ops/main.py
@@ -55,7 +55,7 @@ def _exe_path(path: Path) -> Optional[Path]:
 
 def _create_event_link(
     charm: 'ops.charm.CharmBase',
-    bound_event: 'ops.framework.EventSource',
+    bound_event: 'ops.framework.BoundEvent',
     link_to: Union[str, Path],
 ):
     """Create a symlink for a particular event.
@@ -66,7 +66,7 @@ def _create_event_link(
         link_to: What the event link should point to
     """
     # type guard
-    assert bound_event.event_kind, f'unbound EventSource {bound_event}'
+    assert bound_event.event_kind, f'unbound BoundEvent {bound_event}'
 
     if issubclass(bound_event.event_type, ops.charm.HookEvent):
         event_dir = charm.framework.charm_dir / 'hooks'
diff --git a/ops/model.py b/ops/model.py
index 3b9c4d8..434c0e2 100644
--- a/ops/model.py
+++ b/ops/model.py
@@ -1181,6 +1181,7 @@ class SecretInfo:
         expires: Optional[datetime.datetime],
         rotation: Optional[SecretRotate],
         rotates: Optional[datetime.datetime],
+        description: Optional[str] = None,
     ):
         self.id = Secret._canonicalize_id(id)
         self.label = label
@@ -1188,6 +1189,7 @@ class SecretInfo:
         self.expires = expires
         self.rotation = rotation
         self.rotates = rotates
+        self.description = description
 
     @classmethod
     def from_dict(cls, id: str, d: Dict[str, Any]) -> 'SecretInfo':
@@ -1205,6 +1207,7 @@ class SecretInfo:
             expires=timeconv.parse_rfc3339(expires) if expires is not None else None,
             rotation=rotation,
             rotates=timeconv.parse_rfc3339(rotates) if rotates is not None else None,
+            description=typing.cast(Optional[str], d.get('description')),
         )
 
     def __repr__(self):
@@ -1215,7 +1218,8 @@ class SecretInfo:
             f'revision={self.revision}, '
             f'expires={self.expires!r}, '
             f'rotation={self.rotation}, '
-            f'rotates={self.rotates!r})'
+            f'rotates={self.rotates!r}, '
+            f'description={self.description!r})'
         )
 
 


Test patch:
diff --git a/ops/testing.py b/ops/testing.py
index 72c64e1..02ab2ee 100644
--- a/ops/testing.py
+++ b/ops/testing.py
@@ -2814,6 +2814,7 @@ class _TestingModelBackend:
             expires=secret.expire_time,
             rotation=rotation,
             rotates=rotates,
+            description=secret.description,
         )
 
     def secret_set(
diff --git a/test/test_model.py b/test/test_model.py
index 5a5b362..b72b997 100644
--- a/test/test_model.py
+++ b/test/test_model.py
@@ -3565,6 +3565,7 @@ class TestSecretInfo:
             expires=datetime.datetime(2022, 12, 9, 14, 10, 0),
             rotation=ops.SecretRotate.MONTHLY,
             rotates=datetime.datetime(2023, 1, 9, 14, 10, 0),
+            description='desc',
         )
         assert info.id == 'secret:3'
         assert info.label == 'lbl'
@@ -3572,6 +3573,7 @@ class TestSecretInfo:
         assert info.expires == datetime.datetime(2022, 12, 9, 14, 10, 0)
         assert info.rotation == ops.SecretRotate.MONTHLY
         assert info.rotates == datetime.datetime(2023, 1, 9, 14, 10, 0)
+        assert info.description == 'desc'
 
         assert repr(info).startswith('SecretInfo(')
         assert repr(info).endswith(')')
@@ -3586,6 +3588,7 @@ class TestSecretInfo:
                 'expiry': '2022-12-09T14:10:00Z',
                 'rotation': 'yearly',
                 'rotates': '2023-01-09T14:10:00Z',
+                'description': 'desc',
             },
         )
         assert info.id == 'secret:4'
@@ -3594,6 +3597,7 @@ class TestSecretInfo:
         assert info.expires == datetime.datetime(2022, 12, 9, 14, 10, 0, tzinfo=utc)
         assert info.rotation == ops.SecretRotate.YEARLY
         assert info.rotates == datetime.datetime(2023, 1, 9, 14, 10, 0, tzinfo=utc)
+        assert info.description == 'desc'
 
         info = ops.SecretInfo.from_dict(
             'secret:4',
@@ -3609,6 +3613,7 @@ class TestSecretInfo:
         assert info.expires is None
         assert info.rotation is None
         assert info.rotates is None
+        assert info.description is None
 
         info = ops.SecretInfo.from_dict('5', {'revision': 9})
         assert info.id == 'secret:5'