Skip to content

fix: make _CharmSpec covariant in its charm type - #2715

Merged
tonyandrewmeyer merged 3 commits into
canonical:mainfrom
tonyandrewmeyer:fix/2242-charmspec-covariant
Sep 1, 2026
Merged

fix: make _CharmSpec covariant in its charm type#2715
tonyandrewmeyer merged 3 commits into
canonical:mainfrom
tonyandrewmeyer:fix/2242-charmspec-covariant

Conversation

@tonyandrewmeyer

Copy link
Copy Markdown
Collaborator

_CharmSpec[MyCharm] isn't assignable where _CharmSpec[ops.CharmBase] is expected, because the type parameter is invariant. _CharmSpec is a frozen dataclass, so a covariant parameter is sound, and this adds a separate covariant TypeVar for it rather than changing the public CharmType, which is also used in input positions on Context and Manager and is exported, so flipping its variance would be a change for anyone annotating against it.

autoload is generic now too. It was typed (type[CharmBase]) -> _CharmSpec[CharmBase], so it erased the subclass even before variance came into it, which is why Context.__init__ needed a # pyright: ignore[reportAssignmentType] on the call. That ignore is gone, which is a reasonable check that the fix does what it says.

James asked on the issue whether to add a TypeVar default at the same time, for the ergonomics in cases like #2248. I've left that out: it needs typing_extensions.TypeVar for the Pythons we support, #2248 has since merged with a Context[Any] workaround, and it seems like a separate decision to the variance one.

Fixes #2242

_CharmSpec[MyCharm] wasn't assignable where _CharmSpec[ops.CharmBase]
was expected, because the type parameter was invariant. _CharmSpec is a
frozen dataclass, so a covariant parameter is sound; use a separate
covariant TypeVar rather than changing the public CharmType, which is
also used in input positions on Context and Manager.

Also make autoload generic, so that the spec it returns for a charm
subclass carries the subclass through rather than erasing it to
CharmBase.
Comment thread testing/src/scenario/state.py Outdated
@tonyandrewmeyer
tonyandrewmeyer marked this pull request as ready for review August 31, 2026 02:18

@james-garner-canonical james-garner-canonical left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks, this turned out simpler than I had anticipated it might!

Comment thread testing/src/scenario/state.py Outdated
Comment on lines -2300 to +2304
def autoload(charm_type: type[CharmBase]) -> _CharmSpec[CharmBase]:
def autoload(charm_type: type[CharmType]) -> _CharmSpec[CharmType]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it makes sense that this isn't the covariant type variable -- we're just passing the type through, so an invariant type variable makes sense.

Co-authored-by: James Garner <james.garner@canonical.com>
@tonyandrewmeyer
tonyandrewmeyer merged commit 50e4cb8 into canonical:main Sep 1, 2026
60 checks passed
@tonyandrewmeyer
tonyandrewmeyer deleted the fix/2242-charmspec-covariant branch September 1, 2026 08:25
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.

CharmType TypeVar used by _CharmSpec should probably be covariant

3 participants