Skip to content

Comments

feat: typed testing relation getters#2326

Open
james-garner-canonical wants to merge 9 commits intocanonical:mainfrom
james-garner-canonical:26-02+feat+typed-testing-get-relation
Open

feat: typed testing relation getters#2326
james-garner-canonical wants to merge 9 commits intocanonical:mainfrom
james-garner-canonical:26-02+feat+typed-testing-get-relation

Conversation

@james-garner-canonical
Copy link
Contributor

This PR adds more narrowly typed methods alongside get_relation and get_relations, like get_subordinate_relation and get_peer_relations. These are typed as returning the correct relation type, rather than just RelationBase, removing the need to narrow the relation type afterwards in tests. If the relation type is incorrect, a TypeError is raised.

Resolves #2243.

@james-garner-canonical james-garner-canonical marked this pull request as ready for review February 13, 2026 03:18
Copy link
Collaborator

@tonyandrewmeyer tonyandrewmeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently:

rel = state.get_relation(id)
assert isinstance(rel, testing.Relation)

With this PR:

rel = state_get_regular_relation(id)

With the other proposal in the ticket:

rel = state.get_relation(id, kind=testing.Relation)

Or no helper:

rel = next(iter(r for r in state.relations if r.id ==id and isinstance(r, testing.Relation))

I'm not a big fan of expanding the State API surface (it's already pretty large) with 3 or 6 new methods (and I guess this effectively deprecates get_relation?), but all the asserts are annoying, and the kind= overload solution does feel unpythonic. Presumably most of the time that you're asserting on a relation, it does matter which type it is as well, since you want a specific databag that lives in differently named attributes.

So I think I'm on board with the non-plural ones (tempting to officially deprecate get_relation, so that it's effectively only adding two. I'm much less convinced about the plurals.

return state_relation
raise KeyError(f'relation: id={relation} not found in the State')

def get_regular_relation(self, relation: int, /) -> Relation:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really love the word "regular here", but I'm also not sure what to suggest instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I don't love it either. I went with it because the Juju docs on relations basically go Peer, Non-Peer, Subordinate, Non-Subordinate, and then:

A non-subordinate relation (aka ‘regular’) is a non-peer relation where the applications are both principal.

I'm guessing that's why Dima suggested it too. Open to alternatives though.

r for r in self.relations if _normalise_name(r.endpoint) == normalized_endpoint
)

def get_regular_relations(self, endpoint: str) -> tuple[Relation, ...]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced about adding the plural/endpoint ones. I'm pretty sure when we've looked at use of this in the past, it's almost always code like:

state_in = State(..., relations={relation})
state_out = ctx.run(..., state=state_in)
assert state_out.get_relations('endpoint')[0]...

And that is cleaner as:

state_in = State(..., relations={relation})
state_out = ctx.run(..., state=state_in)
assert state_out.get_relation(relation.id)...

I had a quick look over a few random selections in charms, and they were all of that type. I'm sure there are exceptions, but this is already adding 3 new methods for getting by ID - I'm not convinced there is enough value to add another 3 as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I'll let @dimaqq weigh in too, but I'm happy to remove the plural ones.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose my one hesitation would be that getting relations by endpoint name rather than by ID feels more like the right level of expression for state-transition tests. But the current usage argument is pretty compelling.

def _get_typed_relation(self, relation: int, kind: type[_RelationType]) -> _RelationType:
rel = self.get_relation(relation)
if not isinstance(rel, kind):
raise TypeError(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should be:

  • a TypeError (wrong type of relation about to be returned), or
  • a ValueError (wrong relation: int argument passed in)

Comment on lines +1825 to +1826
for rel in rels:
if not isinstance(rel, kind):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A zero-length return is valid, isn't it?
Then there's a possibility of a false positive, that is not raising an exception when we would have if there was a relation.

@dimaqq
Copy link
Contributor

dimaqq commented Feb 16, 2026

Overall I'm not opposed to it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve typing UX of ops.testing.State.get_relation

3 participants