Closed james-garner-canonical closed 1 month ago
I lean towards simplifying them all -- changing them all to just the enum (or just the str in the case of the dict). People shouldn't be doing this, and as you show, in the cases where we do allow a union things like is_running
are half broken anyway. I suspect the _ServiceInfoDict
allowing enum types at all was a copy-n-paste issue; it seems like a bug.
Let's at least try that and see what it looks like, and run super-tox over the diff to see if there are any failures.
Working towards issue #1287, this draft PR removes the use of
str
andenum
member unions connected to theops.pebble.ServiceInfo
class.In this PR I try to remove all such unions to simplify types. However, I wonder if it would be better to continue to allow the use of both
str
andenum
members as arguments in some cases (constructors) where removing the union might result in a breaking api change.Previously a
str
argument that anops
enum
doesn't know about would result in the use of thestr
, while a known argument would use the correspondingenum
member. Instead, we now use anUNKNOWN
enum member in this case.This is a behaviour change, but shouldn't be a breaking change, because the only time you should be using such values would be if you were developing for a more recent Pebble version than
ops.pebble
knows about. Sinceops.pebble
is presumable up to date currently, this will only be the case on olderops
versions, which this PR does not apply to. However, it will be worth mentioning this behaviour change in the appropriate release notes.An example of the core behaviour change, in the
ServiceInfo.from_dict
method:This leads us to the
__init__
method, where we also have unions to remove. I want to highlight this change because it's potentially breaking.This is a breaking change, as the
pebble
module is public (explicitly exported inops.__init__.__all__
), andServiceInfo
is a public member of that module. Previously a user could expect to be able to callServiceInfo("foo", startup="enabled", current="active")
if they wanted to. However:It never worked correctly with string arguments. Note how
startup
andcurrent
are assigned in__init__
without any runtime validation. This leads to unexpected behaviour here:This may have been written with the assumption that
ServiceStatus.ACTIVE == 'active'
, the way it would with a normal class (is this how enums in other languages most commonly work?). If that were the case, then you'd expect self.current being a union ofstr
andenum
members to just work. Instead,ServiceStatus.ACTIVE
is an object oftype
<enum 'ServiceStatus'>
andServiceStatus.ACTIVE.value == "hello"
, so this will returnFalse
ifself.current == 'active'
.Constructing a
ServiceInfo
from arbitrary arguments isn't really the intended use case, as you want information about actually running services. You're more likely to callops.pebble.get_services
, or a bit lower level, callops.pebble.ServiceStatus.from_dict
with a dict representation of the services.This leads me to think this breaking change may be ok. Currently the change will only result in a type checking error, which if ignored will lead to the same (broken) runtime behaviour as previously.
We could avoid the breaking change and handle things better at runtime (fixing the issue with
is_running
) by continuing to accept unions for the__init__
args and turning them intoenum
members before assigning them. Thefrom_dict
logic could be moved to__init__
to handle this.The final instance of
str
andenum
member unions related to this class is_ServiceInfoDict
. Removing this union leads to another potentially breaking change._ServiceInfoDict
is part of the signature of the publicServiceInfo.from_dict
method, and also used to annotate a value returned from an external Pebble call, which could never actually contain Pythonenum
members. Without introducing more type variables, the correct change when removing these unions should therefore be:But again this is technically a breaking change, as previously a user could expect to validly call:
This still works at runtime, because
ServiceStartup(ServiceStartup.ENABLED)
andServiceStartup('enabled')
both produceServiceStartup.ENABLED
, but potentially introduces a new type checking error for users. Again though, using a hand crafted dictionary rather than one that ultimately comes from Pebble itself is less likely here (what about unit tests?).We could avoid a breaking change here by continuing to accept the original
_ServiceInfoDict
type infrom_dict
.Do we want to remove all the uses of
str
andenum
member unions as a goal in itself to simplify typing? If so, this may lead to some breaking changes.Alternatively, if the goal is just to switch to using an
UNKNOWN
enum member instead of passing along unknown string values, then I think we'll need to keepstr
andenum
member unions in some places.