Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix!: correct the model config types #1183

Merged
merged 18 commits into from
Apr 17, 2024

Conversation

tonyandrewmeyer
Copy link
Contributor

Corrects types related to the model configuration:

  • LazyMapping may have Boolean, integer, or float values, as well as strings
  • _ConfigOption may have a secret value for `type - also move this to testing.py since that's the only place it's used and it's private
  • _ModelBackend.config_get returns a dict with Boolean, integer, float, or string values

The PR also sets a missing return type so that pyright would detect the config_get issue.

There are no tests or pyright changes for the _ConfigOption change: we don't generally rely on pyright rather than unit tests for ensuring the typing is correct, but that doesn't seem feasible here. Let me know if you think we do need something added here to prevent regression.

There are no tests or pyright changes for the LazyMapping change, other than making sure we do have test_model.py exercise each of the config types. I don't think it's possible to have pyright detect this issue - e.g. you can get it to show an error if you try to use an integer config value as an integer (self.config["int-opt"] + 1), but that's still the case after the fix, since it could be a string (float, Boolean, secret) option. The test could have typing.assert_type calls but (a) that's only in Python 3.11, and (b) I'm not convinced we really want to do that. Let me know if you think we do need something added here to prevent regression.

Fixes #1182

ops/model.py Outdated Show resolved Hide resolved
@tonyandrewmeyer
Copy link
Contributor Author

There was a bit more (all minor) breakage than I had expected (it's interesting that so many charms are actually doing static checking on this and yet no-one noticed that the type was wrong - I suspect that says something about how typing is used in Python). However, for all the charms I'm aware of, there should be PRs (big list above) that fix things.

Comment on lines +94 to +103
# Copied from typeshed.
_KT = typing.TypeVar("_KT")
_VT_co = typing.TypeVar("_VT_co", covariant=True)


class _SupportsKeysAndGetItem(typing.Protocol[_KT, _VT_co]):
def keys(self) -> typing.Iterable[_KT]: ...
def __getitem__(self, __key: _KT) -> _VT_co: ...


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'm not thrilled about adding this here. However, I couldn't find any other way to satisfy the way one of the charms (Jenkins, I think) was using relation data.

A minimal example is:

class A(typing.TypedDict):
    foo: str

r = ops.RelationDataContent(None, None, None)  # type:ignore
a = A(foo="hello")
r.update(a)

mypy complains about this:

/tmp/reldata.py:9: error: Argument 1 to "update" of "MutableMapping" has incompatible type "A"; expected "SupportsKeysAndGetItem[str, str]"  [arg-type]
/tmp/reldata.py:9: note: Following member(s) of "A" have conflicts:
/tmp/reldata.py:9: note:     Expected:
/tmp/reldata.py:9: note:         def __getitem__(self, str, /) -> str
/tmp/reldata.py:9: note:     Got:
/tmp/reldata.py:9: note:         def __getitem__(self, str, /) -> object

The charm error is the same.

It seems like it shouldn't be required, because RelationDataContent has an explicit __getitem__ that does return str, and the MutableMapping is [str, str]. It might be that this is a limitation of mypy? pyright does complain too, though:

/tmp/reldata.py
  /tmp/reldata.py:9:1 - error: No overloads for "update" match the provided arguments (reportCallIssue)
  /tmp/reldata.py:9:10 - error: Argument of type "A" cannot be assigned to parameter "__m" of type "Iterable[tuple[str, str]]" in function "update"
    "A" is incompatible with "Iterable[tuple[str, str]]"
      Type parameter "_T_co@Iterable" is covariant, but "str" is not a subtype of "tuple[str, str]"
        "str" is incompatible with "tuple[str, str]" (reportArgumentType)
2 errors, 0 warnings, 0 informations 

_LazyValueType = typing.TypeVar("_LazyValueType")


class _GenericLazyMapping(Mapping[str, _LazyValueType], ABC):
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 added this in the main conversation, but repeating here in context: I would prefer to have this be LazyMapping and just have the generic type made concrete for RelationDataContent (like with ConfigData), but that would be a backwards-incompatible change and isn't actually required to fix the bug.

Comment on lines +1719 to +1722
def update(self, other: _SupportsKeysAndGetItem[str, str], **kwargs: str):
"""Update the data from dict/iterable other and the kwargs."""
super().update(other, **kwargs)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See the note above: this was the only way I could find to satisfy mypy when using relation.data[x].update({...}) where the value passed was a TypedDict.

Copy link
Contributor

@addyess addyess May 22, 2024

Choose a reason for hiding this comment

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

@tonyandrewmeyer i just discovered this change has broken an untested situation:

this module previously could execute

some_dict = {"foo": "bar"}
self.relation.data[self.charm.model.unit].update(**some_dict)

but now there's an exception because i don't have an other argument

  File "/var/lib/juju/agents/unit-kubernetes-worker-0/charm/venv/ops/interface_aws/requires.py", line 133, in _joined
    self._request({"instance-id": self.instance_id, "region": self.region})
  File "/var/lib/juju/agents/unit-kubernetes-worker-0/charm/venv/ops/interface_aws/requires.py", line 190, in _request
    self._to_publish.update(**kwds)
TypeError: RelationDataContent.update() missing 1 required positional argument: 'other'

Copy link
Contributor

@addyess addyess May 22, 2024

Choose a reason for hiding this comment

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

here's an example where both styles are acceptable in an update

Python 3.12.3 (main, May 22 2024, 13:41:37) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = {}
>>> a.update(**{"a": 1})
>>> a
{'a': 1}
>>> b = {"b": 2}
>>> a.update(b)
>>> a
{'a': 1, 'b': 2}
>>> c, d = {"c": 3}, {"d": 4}
>>> a.update(c, **d)
>>> a
{'a': 1, 'b': 2, 'c': 3, 'd': 4}

Copy link
Collaborator

@benhoyt benhoyt left a comment

Choose a reason for hiding this comment

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

Ugh, what a mess Python typing is. Thanks for your effort here. It's rather annoying for charmers to have type cast(str, ...) all their config values now, but it does seem correct.

If config was a Mapping[str, Any], would they still have to `cast(str, ...)?

@tonyandrewmeyer
Copy link
Contributor Author

If config was a Mapping[str, Any], would they still have to `cast(str, ...)?

I don't think so, although I didn't try it with the more complicated situations (like the one where update believes an object is being returned). If I understand correctly, in general Any works as some sort of magic 'can match anything', so you can do a = b where a needs to be a str and b is Any, even though it really seems like you only ought be able to do b = a (ie. "b can be anything, so a str is fine" rather than "a has to be a str, so 'no idea what it is' is fine").

I think it does then kind of 'pollute' the type checking, although if it's only being used once quickly afterwards (which was the case in some places, like getting written directly out into application config envars) it doesn't really matter. I think it's nicer if we avoid the "turn off type checking for this thing" behaviour.

For what it's worth, a few charms have merged the appropriate change without (so far) any charmers saying "ugh, I'm going to hate having these casts".

@tonyandrewmeyer tonyandrewmeyer merged commit fa185b4 into canonical:main Apr 17, 2024
26 checks passed
@tonyandrewmeyer tonyandrewmeyer deleted the fix-config-types-1182 branch April 17, 2024 05:07
chanchiwai-ray added a commit to canonical/hardware-observer-operator that referenced this pull request May 2, 2024
After operator framework fixes the incorrect types in `model.config`, it
introduces some issues in our existing codebases since we used to assume
them to have type of `str` which is not correct in the first place.

[1] canonical/operator#1183
tonyandrewmeyer added a commit that referenced this pull request May 24, 2024
…ld be optional (#1226)

Fixes the signature for `RelationDataContent.update` to match
`MutableMapping`, where `other` is optional (a regression introduced in
#1883).

The type for `other` has been simplified to `Any`. It should really be
`Mapping|_SupportsKeysAndGetItem[str,str]` plus a minimal type that
supports `.values`, but it was already messy pulling in
`_SupportsKeysAndGetItem` in #1183, and we're just passing this through
to `MutableMapping` so it doesn't seem like the tight typing is
providing enough benefit to justify the complexity of the signature.
[typeshed has three
overloads](https://github.com/python/typeshed/blob/f7c03486ee01c8ea74823db75e017341bf3c2ad0/stdlib/typing.pyi#L726),
so we could match that (as we did in #1883, just incompletely), if that
is desirable.

Fixes: #1225

---------

Co-authored-by: Tony Meyer <tony.meyer@canonical.com>
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.

Model config types are incorrect
4 participants