pythontype-hintingdjango-manage.pytyper

type hinting for abstract method with fuzzy number of parameters


I work on my pet-project on FastAPI and I want to write django-like manager (manage.py with its commands). I use Typer for this CLI, and I have the following structure of my management sub-module in my app:

...
./management
├── __init__.py
├── app.py
├── collector.py
└── commands
   ├── __init__.py
   ├── base.py
   ├── echo.py
   └── runserver.py

So, I have app.py as builder for management CLI app, collector.py as importer of my commands (to not make manual import every time, when I create new command. It use import_module from importlib) and sub-sub-module commands, that contains all my commands and their interface. All commands should implement 1 method action, so my question is the following: "How could I specify abstract BaseCommand class with action method, if this method will have not a specific amount of parameters per child class? (like, echo has just echo string parameter, and runserver has port as int, host as string and some other parameters)". **kwargs is not a solution, because Pylance mark it as error. For example, for the following command implementation:

# base.py
class BaseCommand(ABC):
    @abstractmethod
    def action(self, **kwargs: Any) -> Any:
        pass


# echo.py
class Echo(BaseCommand):
    def action(self, echo: str):
        """Common echo command. Just """
        print(echo)

Pylance shows me the following error:

Method "action" overrides class "BaseCommand" in an incompatible manner
  Positional parameter count mismatch; base method has 2, but override has 2

So, if I have incorrect CLI app structure, an additional question is the following: "How should I structure my app?"


Solution

  • In order to persuade Pyright (and hence Pylance) that what you're doing is legitimate you need to do to things:

    The result of these two changes is this:

    class BaseCommand(ABC):
        @abstractmethod
        def action(self, **kwargs) -> Any:
            pass
    
    
    class Echo(BaseCommand):
        def action(self, *, echo: str, **kwargs):
            """Common echo command. Just """
            print(echo)
    

    The first * in action says: "everything after this is keyword-only".

    If you don't like the fact that your IDE tells you that kwargs are unused in Echo.action you can replace **kwargs with **_.

    This can be slightly simplified if you are allowed to change the definition of BaseCommand.action:

    class BaseCommand(ABC):
        @abstractmethod
        # Allow positional args in the abstractmethod
        def action(self, *args, **kwargs) -> Any:
            pass
    
    
    class Echo(BaseCommand):
        # Then you don't need `, *,` in the child
        def action(self, echo: str, **kwargs):
            """Common echo command. Just """
            print(echo)