python3: type annotations and enum subclasses - python

I don't like getting complaints from Mypy about function signatures, but I don't know how to resolve this one.
I am building a package, that is to be used by several programs. I have a subclass of IntEnum (FWIW, called _Event), which contains a number of relevant properties and methods. _Event is never used directly, since it contains no members, but several different programs use incompatible subclasses of _Event (AlphaEvent, BetaEvent, etc.), which define the actual members of the Enum (i.e, the actual events). Only one member of each subclass is common, and as it happens, it's called END. Since you can't subclass an enum with members, it is defined in each subclass individually (so _Event has no END member, but AlphaEvent.END and BetaEvent.END exist).
I have several functions which utilise subclasses of _Event. I have a couple which need to access properties of the END member, but are generic to all instances. So they contain a signature:
def generic_event_func(events: _Event):
...
events.END.action = <expr>
MyPy flags the last line of code above with "error: "_Event" has no attribute "END"
True enough, but the subclasses do. How do I annotate the function signature to remove this error?

I faced a kinda similar issue recently and ended refactored using ABC. Not sure if you have latitude to refactor much, but maybe it can help in some way:
from abc import ABC, abstractmethod
from enum import Enum
class Event(Enum):
a = EventA
b = EventB
c = EventC
class AbstractEvent(ABC):
#abstractmethod
def action(self):
pass
class EventA(AbstractEvent):
def action(self):
....
event_cls = Event["a"].value
event: Union[EventA, EventB, EventC] = event_cls()
event.action()

I'm not (yet) an optional static typing person, but something like the following might work:
from typing import Any
class _Event(IntEnum):
END: Any
End is now type hinted, but doesn't actually exist, so won't interfere with subclassing _Event.

Related

type hint for generic attribute inside abstract class

I have a convoluted problem with types that I canĀ“t solve.
Let's say I have some abstract classes and a generic type equivalent to this
from abc import ABC
from typing import Generic, TypeVar
class Action(ABC):
pass
ActionTypeVar = TypeVar("ActionTypeVar", bound="Action")
class Agent(ABC, Generic[ActionTypeVar]):
actions: tuple[ActionTypeVar, ...]
def get_action() -> ActionTypeVar:
return self.action[0]
This works as intended. But I need is to define a function similar to get_action outside the class(in a different package in fact).
def get_action_outside_class(agent: Agent) -> ActionTypeVar:
return agent.actions[0]
The problem is that here the return type is not precise anymore, since we are outside the scope of the class. I wanted to indicate this is of the same type as the elements of agent.actions.
I have tried referencing the elements of Agent.actions in the return of get_action but I can't find out a proper way to do it.
You aren't using Agent generically.
def get_action_outside_class(agent: Agent[ActionTypeVar]) -> ActionTypeVar:
return agent.actions[0]
(Any TypeVar would do; it doesn't need to be the same one you used when defining Agent.)

How to implement FurlEnum (mixin type) to held urls as objects?

I'm trying to implement enum which keeps urls in furl objects. Something like this:
class FurlEnum(furl, Enum):
SAMPLE_URL = furl('https://stackoverflow.com/')
Implementing it this way, however, leads to an error: TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases. I managed to solve this by creating a metaclass that inherits from the base class of Enum and furl:
class FurlEnumMeta(type(furl), type(Enum)):
...
class FurlEnum(furl, Enum, metaclass=FurlEnumMeta):
value: furl
def __str__(self):
return self.url
SAMPLE_URL = furl('https://stackoverflow.com/')
This solved the problem of creating an enum class, but the solution has one significant problem that I do not know how to solve.
Let's take something like this as an example:
sample_url = FurlEnum.SAMPLE_URL
print(sample_url.set(path='questions'))
print(FurlEnum.SAMPLE_URL.value)
print(FurlEnum.SAMPLE_URL)
The result is:
https://stackoverflow.com/questions
https://stackoverflow.com/
https://stackoverflow.com/questions
As you can see, the value of SAMPLE_URL was overwritten by the "set" operation with the original value preserved in the "value" field. Do you know how to prevent it, to get:
https://stackoverflow.com/questions
https://stackoverflow.com/
https://stackoverflow.com/
as a result of above prints?
I was trying different approaches, but none of them worked as I've expected.
For the first problem, why does furl need an ABC metaclass? There is only one abstract method, which a base class __init__ (or __new__) could check for -- also, if furl is not meant to itself be subclassed by other users, the abstract base class enforcements are less useful.
For the second problem: you've made the values of the FurlEnum be a furl, and also to have a furl as the value. (By default, the .value of a mixed enum member is the same type and value as the member itself, but not the same -- in other words, if you mutate one of them, the other is then out of sync.)
How I would solve this problem:
Remove the ABC dependency, which eliminates the need for a FurlEnum metaclass.
Make furls be immutable -- like tuple, str, int, etc -- so that the various operations that change the furl value instead return a new furl, leaving the original one untouched.
Now sample_url.set(path='questions') will return a new, separate furl value, leaving FurlEnum.SAMPLE_URL unchanged.
Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

How to get Mypy working with multiple mixins relying on each other?

Currently in Electrum we use the Union type on self to be able to access methods from multiple mixed-in parent classes. For example, QtPluginBase relies on being mixed into a subclass of HW_PluginBase to work. For example, a valid use is class TrezorPlugin(QtPluginBase, HW_PluginBase).
There is the Qt gui, the Kivy gui, and there is also CLI. Although hardware wallets are not implemented for Kivy, they could be in the future. You can already use them on the CLI.
However there are also multiple hardware wallet manufacturers, all with their own plugins.
Consider Trezor + Qt:
For Qt, we have this class hierarchy:
electrum.plugins.hw_wallet.qt.QtPluginBase used by
electrum.plugins.trezor.qt.QtPlugin(QtPluginBase)
For Trezor, we have:
electrum.plugin.BasePlugin used by
electrum.plugins.hw_wallet.plugin.HW_PluginBase(BasePlugin) used by
electrum.plugins.trezor.trezor.TrezorPlugin(HW_PluginBase)
And to create the actual Qt Trezor plugin:
electrum.plugins.trezor.qt.Plugin(TrezorPlugin, QtPlugin)
The point is that the base gui-neutral plugin will first gain manufacturer-specific methods; then it will gain gui-specific methods.
Aaron (in the comments) suggests that QtPluginBase could subclass HW_PluginBase, but that would mean that the manufacturer-specific stuff would come after, which means the resulting classes cannot be used by the CLI or Kivy.
Note that both
electrum.plugins.trezor.trezor.TrezorPlugin(HW_PluginBase)
and
electrum.plugins.hw_wallet.qt.QtPluginBase
rely on HW_PluginBase. They can't both subclass it.
So if we avoid mix-ins, then the only alternative would be to either have QtPluginBase subclass TrezorPlugin (but there are many manufacturers), or TrezorPlugin could subclass QtPluginBase but then, again, the resulting classes cannot be used by the CLI or Kivy.
I realize that Union is an "or", so the hint is indeed not making sense. But there is no Intersection type. With Union, most of the PyCharm functionality works.
One thing that would be nice is if QtPluginBase could have a type-hint that it subclasses HW_PluginBase, but without actually subclassing at runtime.
How could this be typed with Mypy without having to use this hacky Union type hint on every method (since every method has self)?
With the Protocols added in PEP-544 (Python 3.8+), you can define the intersection interface yourself! This also lets you hide implementation details in ClassA that you don't want ClassB to use.
from typing import Protocol
class InterfaceAB(Protocol):
def method_a(self) -> None: ...
def method_b(self) -> None: ...
class ClassA:
def method_a(self) -> None:
print("a")
class ClassB:
def method_b(self: InterfaceAB) -> None:
print("b")
self.method_a()
# if I remove ClassA here, I get a type checking error!
class AB(ClassA, ClassB): pass
ab = AB()
ab.method_b()
# % mypy --version
# mypy 0.761
# % mypy mypy-protocol-demo.py
# Success: no issues found in 1 source file
Credits to SomberNight/ghost43 for the initial version of this file.
Since mypy doesn't offer an Intersection type yet, you can't type the self arg correctly (and the Union is not a replacement for that!). What you can do is introducing base classes for mixins for type checking only. This is a trick I often use when working with mixins in Django projects. Example:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .plugin import HW_PluginBase
_Base = HW_PluginBase
else:
_Base = object
class QtPluginBase(_Base):
def load_wallet(self, wallet: 'Abstract_Wallet', window: ElectrumWindow):
...
You can now drop the explicit typing of self since mypy can infer all necessary base classes itself.

Can I use Mypy stubs as interfaces?

Mypy allows us to write class stubs which can be placed in the same directory as the actual class. This stub is very similar to an interface as known from other languages. Is it possible to have a client use the stub and the implementation strictly follow the stub?
Example I would like to work:
class IDependency:
def do_something(self) -> None: ...
def do_something_else(self) -> None: ...
class Service:
def __init__(self, dependency: IDependency):
dependency.do_something()
dependency.do_something_else() # this fails silently
class DependencyImplementation(IDependency):
def do_something(self) -> None:
print("doing something")
# Note there is no `do_something_else` here.
This works. However, if DependencyImplementation doesn't implement the do_something method, there is no error from Mypy and no error from Python itself. The call just doesn't do anything. Do I have to write raise NotImplementedException() or annotate each method with #abc.abstractmethod for this to work? Is there some special flags in Mypy or the Python interpreter?
Is this a use case for Mypy Protocols? It seems to be coming soon (maybe Python 4?)
This is indeed something you can do using either #abc.abstractmethod or protocols. The former is akin to using Java's abstract classes; the latter will be kin to using Go's interfaces or Rust traits.
Here is an example that uses ABCs:
from abc import abstractmethod
class Parent:
#abstractmethod
def foo(self) -> None: ...
# Missing an implementation for 'foo'!
class Child(Parent): pass
print(Child()) # Error: Cannot instantiate abstract class 'Child' with abstract attribute 'foo'
A few things to note about this example:
You get an error on the instantiation of the Child class, not on the declaration. This is to support the use case where you never instantiate Child, but instead subclass it again and define foo in that second subclass.
We don't need to add the usual abc metaclass to Parent ( e.g. class Parent(metaclass=ABCMeta)): mypy will understand what #abc.abstractmethod means with or without it. Include the metaclass only if you want the Python interpreter to also enforce that you've correctly overridden anything marked as being abstract at runtime.
ABCs are not quite interfaces -- you can still define fields and non-abstract methods. They're more akin to Java-style abstract classes.
You can also use protocols, though for now you'll need to first pip install typing_extensions to use it. Here's an example:
from typing_extensions import Protocol
class CanFoo(Protocol):
def foo(self) -> None: ...
class Child: pass
def expects_fooable(x: CanFoo) -> None: ...
x = Child()
expects_fooable(x) # Error: Argument 1 to "expects_fooable" has incompatible type "Child"; expected "CanFoo"
A few notes:
Here, Child deliberately does not inherit from CanFoo: there is no explicit link between a class and the protocol(s) it implements: protocols are very similar to Go-style interfaces and can be more ad-hoc. Contrast this to languages like Java, where you do need to include a "implements Blah" in the class definition.
Unlike the previous error, we do not get an error on the instantiation of Child: there's nothing inherently wrong with it. Rather, we get an exception when we try using it improperly.
A few final notes:
Stub files may superficially look like interfaces, but they're really not: they're more just a way to bring types to code which we cannot easily modify and add type hints to. You can think of them being vaguely similar to C-style header files: it's a way of storing the signatures of existing objects independently from the source code.
"Typeshed" is the name of a specific project which includes stubs for the standard library and a few popular 3rd party modules. That word is not a synonym for "stub files". Similarly, the term "class stub" is also a bit of a misnomer: there are only stub files, which may or may not contain definitions for classes. (If the original Python or C extension library you're trying to type contains only functions, the corresponding stub file would also likely only contain signatures for those functions.)

Can python class variables become instance variables when altered in __init__?

As far as I understand var is a class variable here:
class MyClass:
var = 'hello'
def __init__(self):
print(self.var)
And thats an instance variable:
class MyClass:
def __init__(self, var):
self.var = var
print(self.var)
I had the problem, that I was looking for a method to make type hinting possible for instance variables. I can of course typehint the parameter with def __init__(self, var: str): but that would not effect the instance variable itself.
Then I noticed in some descriptions (like here) that they used the term instance variable for a var like this:
class MyClass:
var : str = 'hello'
def __init__(self, var : str = None):
self.var = var if var
print(self.var)
That would be the solution indeed, but is that still an instance variable? Because it is defined in the class body, it would be a class variable in my understanding. If you would use a list for var, all alterations to this list-var would be shared over the instances.
But in this case there would be no problem, because the string is replaced and would not be shared for other instances. However, it seems wrong to me if you call it an instance variable and I don't know if I should use it like this just to have the type hinting working.
That would be the solution indeed, but is that still an instance variable? Because it is defined in the class body, it would be a class variable in my understanding. [...snip...] However, it seems wrong to me if you call it an instance variable and I don't know if I should use it like this just to have the type hinting working.
For what it's worth, I also share the same discomfort. It seems like we're conceptually mixing two concepts there just for the sake of having cleaner type annotations.
However, I've asked Guido one or two times about this, and it seems like he does indeed prefers treating those class attributes as if they were instance attributes.
In any case, to answer your core question, if we do this:
class Test:
field1: int
field2: str = 'foo'
Then...
PEP 484 and 526 compliant type checkers will treat this class as if:
It has an instance attribute named field1
It has an instance attribute named field2 that has a default value of 'foo' (as per PEP 526).
At runtime, ignoring type hints, Python will:
Add a class annotation named field1 to Test, but not a class attribute. (Class annotations are not automatically turned into class attributes.)
Add both a class annotation named field2 to Test as well as a class attribute named field2 containing the value 'foo'.
So, it can get a bit muddled.
But regardless, this then begs the question: how do we indicate to a type checker that we want some field to genuinely be a class attribute?
Well, it turns out PEP 484 was amended semi-recently to contain the ClassVar type annotation, which does exactly that.
So, if we wanted to add a new class attribute, we could do this:
from typing import ClassVar
class Test:
field1: int
field2: str = 'foo'
field3: ClassVar[int] = 3
So now, field3 should be treated as a class attribute with a default value of '3'.
(Note: ClassVar was added to typing for Python 3.5.3 -- if you're using the older version of typing bundled with Python 3.5, you can get a "backport" of the type by installing the typing_extensions third part module via pip and importing ClassVar from there instead.)
I think whether you decide to embrace this approach or not use it is a personal preference.
On one hand, Guido's opinion, pretty much by definition, defines what's "Pythonic" or not, so from that stance, there's no issue adopting this new idiom. Furthermore, the language itself is slowly but surely shifting to adopt this new idiom -- see the very recently accepted PEP 557, for example, which ends up following this same idiom of treating class attributes/class annotations as instance attributes.
On the other hand, it's difficult to shake off the nagging worry that this subtle difference will lead to issues down the line. In that case, you could stick with the standard approach of just setting all your fields inside __init__. This approach also has the benefit of keeping your code compatible with Python 2 and 3.x - 3.5.
A middle ground might be to just simply never use class attributes, in any way, shape, or form, and just stick to using class annotations. This is slightly restrictive, since we can no longer give our instance variables default values, but we can now avoid conflating class attributes with instance attributes entirely. (As previously stated, and as pointed out in the comments, class annotations are not added as class attributes.)

Categories