1

From abstract super-class enumerations Action and Activity there are the inherited enumerations:

  • ActivityA upon which ActionA can be performed; and
  • ActivityB upon which ActionB can be performed.

How can a type declaration be added to the action argument of the perform method on the abstract Activity method such that MyPy will respect the inheritance or which action applies to which activity?

from abc import ABC, ABCMeta, abstractmethod
from enum import EnumMeta, IntEnum


class ABCEnumMeta(EnumMeta, ABCMeta):
    ...
    

class Action(ABC, IntEnum, metaclass=ABCEnumMeta):
    ...


class ActionA(Action):
    start = 1
    stop = 2

class ActionB(Action):
    start = 1
    pause = 2
    resume = 3
    complete = 4
    fail = 5

class Activity(ABC, IntEnum, metaclass=ABCEnumMeta):
    @abstractmethod
    def perform(
        self: "Activity",
        action,                                      # <- This line
    ) -> str:
        ...

class ActivityA(Activity):
    this = 1
    that = 2
    
    def perform(
        self: "ActivityA",
        action: ActionA,
    ) -> str:
      return f"A: {action.name} {self.name}"

        
class ActivityB(Activity):
    something = 1
    another = 2
    
    def perform(
        self: "ActivityB",
        action: ActionB,
    ) -> str:
      return f"B: {action.name} {self.name}"


print( ActivityB.something.perform(ActionB.pause) )
print( ActivityA.this.perform(ActionA.stop) )
print( ActivityB.another.perform(ActionA.start) )
print( ActivityA.that.perform(ActionB.fail) )

The mypy.ini settings file being used is:

[mypy]
disallow_any_expr        = True
disallow_any_decorated   = True
disallow_any_explicit    = True
disallow_any_generics    = True
disallow_subclassing_any = True
disallow_untyped_calls   = True
disallow_untyped_defs    = True
disallow_incomplete_defs = True

The output from MyPy is:

test_enum.py:26: error: Function is missing a type annotation for one or more arguments
test_enum.py:26: error: Type of decorated function contains type "Any" ("Callable[[Activity, Any], str]")
test_enum.py:56: error: Argument 1 to "perform" of "ActivityB" has incompatible type "ActionA"; expected "ActionB"
test_enum.py:57: error: Argument 1 to "perform" of "ActivityA" has incompatible type "ActionB"; expected "ActionA"

(The last two errors are expected.)

In this situation, I would normally use generics and define:

A = TypeVar("A", bound=Action)


class Activity(ABC, Generic[A], IntEnum, metaclass=ABCEnumMeta):
    @abstractmethod
    def perform(
        self: "Activity",
        action A,
    ) -> str:
        ...


class ActivityA(Activity[ActionA]):
    ...


class ActivityB(Activity[ActionB]):
    ...

However, the Enum class raises exceptions when you try to add in a generic.

How could this be solved to properly define the argument types of the abstract method?

MT0
  • 143,790
  • 11
  • 59
  • 117

1 Answers1

0

A partial solution (since an Enum cannot have a Generic mixin) is to use the super-type Action in the abstract parent method (rather than trying to use the sub-types via generics):

class Activity(ABC, IntEnum, metaclass=ABCEnumMeta):
    @abstractmethod
    def perform(
        self: "Activity",
        action: Action,
    ) -> str:
        ...

This will then give the errors:

test_enum.py:36: error: Argument 1 of "perform" is incompatible with supertype "Activity"; supertype defines the argument type as "Action"
test_enum.py:36: note: This violates the Liskov substitution principle
test_enum.py:36: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
test_enum.py:47: error: Argument 1 of "perform" is incompatible with supertype "Activity"; supertype defines the argument type as "Action"
test_enum.py:47: note: This violates the Liskov substitution principle
test_enum.py:47: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
test_enum.py:56: error: Argument 1 to "perform" of "ActivityB" has incompatible type "ActionA"; expected "ActionB"
test_enum.py:57: error: Argument 1 to "perform" of "ActivityA" has incompatible type "ActionB"; expected "ActionA"

The additional errors can be explicitly locally silenced using # type: ignore[override]:

class ActivityA(Activity):
    this = 1
    that = 2
    
    def perform(  # type: ignore[override]
        self: "ActivityA",
        action: ActionA,
    ) -> str:
      return f"A: {action.name} {self.name}"

        
class ActivityB(Activity):
    something = 1
    another = 2
    
    def perform(  # type: ignore[override]
        self: "ActivityB",
        action: ActionB,
    ) -> str:
      return f"B: {action.name} {self.name}"

Once those errors are locally silenced then the output is just the two expected errors:

test_enum.py:56: error: Argument 1 to "perform" of "ActivityB" has incompatible type "ActionA"; expected "ActionB"
test_enum.py:57: error: Argument 1 to "perform" of "ActivityA" has incompatible type "ActionB"; expected "ActionA"

While this makes it explicitly clear that the abstract parent method should take an Action type in the parameter, it does not indicate from the abstract parent class that the children should be expecting a specific sub-type (as generics would allow). Therefore, this is only a partial solution; a better solution would be to "fix" the Enum class (or to sub-class Enum) to allow a Generic mixin.

MT0
  • 143,790
  • 11
  • 59
  • 117