Search code examples
pythonpython-3.xmypycommand-pattern

mypy does not recognize a method of a class


from abc import ABC, abstractmethod
from typing import List


class AirConditioner:
    """Class that represents an air conditioner"""

    def __init__(self, identify: str, state: bool, temperature: int):
        self.identify = identify
        self._state = state
        self._temperature = temperature

    def turn_on(self) -> None:
        self._state = True

    def turn_off(self) -> None:
        self._state = False

    def set_temperature(self, temperature: int) -> None:
        self._temperature = temperature

    def get_state(self) -> bool:
        return self._state

    def get_temperature(self) -> int:
        return self._temperature


class ICommand(ABC):
    """Interface that represents a command"""

    @abstractmethod
    def execute(self) -> None:
        pass

    @abstractmethod
    def undo(self) -> None:
        pass


class TurnOnAirConditioner(ICommand):
    """Class that represents a command to turn on an air conditioner"""

    def __init__(self, air_conditioner: AirConditioner):
        self._air_conditioner = air_conditioner

    def execute(self) -> None:
        self._air_conditioner.turn_on()

    def undo(self) -> None:
        self._air_conditioner.turn_off()


class ChangeTemperatureAirConditioner(ICommand):
    """Class that represents a command to change the temperature of an air conditioner"""

    def __init__(self, air_conditioner: AirConditioner):
        self._air_conditioner = air_conditioner
        self._temperature = air_conditioner.get_temperature()
        self._temperature_anterior = self._temperature

    def set_temperature(self, temperature: int) -> None:
        self._temperature_anterior = self._temperature
        self._temperature = temperature

    def execute(self) -> None:
        self._air_conditioner.set_temperature(self._temperature)

    def undo(self) -> None:
        self._air_conditioner.set_temperature(self._temperature_anterior)


class Aplicativo:
    """Class that represents an application that uses the command pattern to control an air conditioner"""

    def __init__(self) -> None:
        self._comandos: List[ICommand] = []

    def set_comando(self, comando_app: ICommand) -> int:
        self._comandos.append(comando_app)
        return len(self._comandos) - 1

    def get_command(self, comando_id: int) -> ICommand:
        return self._comandos[comando_id]

    def pressing_button(self, comando_id: int) -> None:
        self._comandos[comando_id].execute()


if __name__ == "__main__":
    app = Aplicativo()

    my_air_conditioner = AirConditioner("Air Conditioner", False, 26)

    change_temperature_air = ChangeTemperatureAirConditioner(my_air_conditioner)
    turn_on_ar = TurnOnAirConditioner(my_air_conditioner)

    ID_TURN_AIR_ON = app.set_comando(turn_on_ar)
    ID_CHANGE_AIR_TEMPERATURE = app.set_comando(change_temperature_air)

    app.pressing_button(ID_TURN_AIR_ON)
    comando = app.get_command(ID_CHANGE_AIR_TEMPERATURE)
    comando.set_temperature(25)

When I run the code above, mypy brings me the following alert:

error: "ICommand" has no attribute "set_temperature" [attr-defined]

How do I do it when I need to call a method, but not every class that implements the ICommand interface has this method?

I tried to comment lines with #type ignore, but I would like to know a better way to handle this problem


Solution

  • Following mypy guidelines:

    In cases where your code is too magical for mypy to understand, you can make a variable or parameter dynamically typed by explicitly giving it the type Any

    source: https://mypy.readthedocs.io/en/stable/dynamic_typing.html

    The best way to solve this problem is change get_command method, in Aplicativo class to:

    from abc import ABC, abstractmethod
    from typing import List, Any
        
        ...
    
    class Aplicativo:
            """Class that represents an application that uses the command pattern to control an air conditioner"""
        
            ...
    
            def get_command(self, comando_id: int) -> Any:
                return self._comandos[comando_id]
    
            ...
    

    Solving the issue with mypy