1

This code runs fine, but mypy complains:

class NamePrintingSession(requests.Session):
    def request(self, method, url, name=None, **kwargs):
        print(name)
        return super().request(method, url, **kwargs)


nps = NamePrintingSession()
nps.get("http://www.google.com", name="wooo") # error: Unexpected keyword argument "name" for "get" of "Session"

I kind of understand WHY it fails: Althought the definition of get() in requests accepts **kwargs, the stubs have some sort of additional information, and that doesnt change just because request() in my derived class accepts an additional parameter.

Is it possible to type-hint the get, post, delete, etc methods in my subclass with the parameters from the parent class but add the name parameter? (preferably without explicitly re-stating every parameter and also keeping the documentation links intact)

Cyberwiz
  • 11,027
  • 3
  • 20
  • 40
  • 1
    Updated my code to show that `name` is optional. The derived get method allows additional parameters but is fine with having the same ones as the base version. – Cyberwiz Jun 03 '22 at 11:12
  • Look closer at `request` definition in stubs. It doesn't include a star, so call like `session.request('GET', uri, 'q=something')` is perfectly valid. This is incompatible with your definition ('q=something' will be assigned to wrong parameter). However, overriding `get` or `post` this way is okay, because everything except `url` is kwonly in parent class. – STerliakov Jun 03 '22 at 12:04
  • Ok, I guess I could add `*args` to my request definition (before `name=`) and pass it to the parent method similarly, to ensure better compatibility. But I feel we're getting off topic here, because this doesn't help with my actual problem. – Cyberwiz Jun 03 '22 at 12:21
  • About preserving `**kwargs` signature, see [this question](https://stackoverflow.com/questions/72304369/inferring-argument-types-from-args/72329755#72329755). Decorator approach will work, you'll have to repeat all parameters only once. – STerliakov Jun 09 '22 at 09:58

2 Answers2

1

The implementation of the get method at sessions.py is the following

# sessions.py

class Session:
    ...

    def get(self, url, **kwargs):
        r"""Sends a GET request. Returns :class:`Response` object.

        :param url: URL for the new :class:`Request` object.
        :param \*\*kwargs: Optional arguments that ``request`` takes.
        :rtype: requests.Response
        """

        kwargs.setdefault('allow_redirects', True)
        return self.request('GET', url, **kwargs)

So it uses the request method for its implementation, setting GET as a request method. Overriding the arguments of the request method changes only the typing behavior of the request method.

The base implementation allows any keyword argument as it defines **kwargs, but it also has an interface declared at the sessions.pyi file. .pyi files are called stub files and are used to separate implementation from typing interfaces. If the implementation does not contain any typing hints, the IDE looks at the .pyi declaration.

As we can see, all possible keyword arguments of the get method are declared at sessions.pyi.

# sessions.pyi

class Session:
    ...

    def get(
        self,
        url: Text | bytes,
        params: _Params | None = ...,
        data: Any | None = ...,
        headers: Any | None = ...,
        cookies: Any | None = ...,
        files: Any | None = ...,
        auth: Any | None = ...,
        timeout: Any | None = ...,
        allow_redirects: bool = ...,
        proxies: Any | None = ...,
        hooks: Any | None = ...,
        stream: Any | None = ...,
        verify: Any | None = ...,
        cert: Any | None = ...,
        json: Any | None = ...,
    ) -> Response: ...

The issue appeared as the interface did not contain the name keyword argument.

So what is happening when we call the get method on the object of the NamePrintingSession?

  • As the NamePrintingSession does not implement the get method, it will call the get method implementation of the Session class.
  • As the get implementation at sessions.py does not contain typing hints, the IDE looks for .pyi file of the sessions.py, which should be sessions.pyi, and if any interface of get is found, then uses it. If the get method did not have an interface, any keyword arguments would be allowed because of the **kwargs of the base implementation.

To fix that issue, you can customize the implementation of the get method for the NamePrintingSession class. But this solution is not optimal if the name argument should not affect the request behavior. In other words, if the name is used only in the request method, then there is no reason to override the rest methods like

# customSession.py

import requests


class NamePrintingSession(requests.Session):
    def request(self, method, url, name=None, **kwargs):
        print(name)
        return super().request(method, url, **kwargs)

    def get(self, url, name=None, **kwargs):
        print(name)
        super().get(url, **kwargs)

Another solution is that you can create a stub file and declare a new interface for your NamePrintingSession class. Suppose the file that contains the NamePrintingSession class is called customSession.py. Then you should create a file with the name customSession.pyi in the same directory.

# customSession.pyi

import requests


class NamePrintingSession(requests.Session):
    def request(self, method, url, name=None, **kwargs):
        ...

    def get(self, url, name=None, **kwargs):
        ...
Artyom Vancyan
  • 5,029
  • 3
  • 12
  • 34
  • 1
    Your second solution sounds like what I am after! although I guess I will have to repeat the parameters that I dont change to make it still check those? I’ll try it out.. – Cyberwiz Jun 06 '22 at 13:32
  • Where does the `get` stub you provide come from? In [typeshed](https://github.com/python/typeshed/blob/master/stubs/requests/requests/sessions.pyi#L119=) (and so in `types-requests` package) it is defined with all keyword-only args except `url`. It is extremely important here, because without that star such override would be not allowed. Also please note that IDE != mypy. – STerliakov Jun 08 '22 at 23:03
  • @SUTerliakov, I copied it from `/snap/pycharm-professional/285/plugins/python/helpers/typeshed/stubs/requests/requests/sessions.pyi`. They are very similar but it seems pycharm uses its own version. – Artyom Vancyan Jun 09 '22 at 06:22
  • 1
    The customSession.pyi approach works, but is it possible to get the param list from the superclass, so I dont have to repeat all the optional parameters? (data, headers etc) (just using **kwargs like in your answer makes it so that any keyword arguments are accepted) – Cyberwiz Jun 09 '22 at 06:35
  • Unfortunately, it is not possible to inherit function parameters from another one. – Artyom Vancyan Jun 09 '22 at 06:39
  • @ArtyomVancyan funny, it's worth issue/PR, because this allows calls that raise on runtime. – STerliakov Jun 09 '22 at 10:21
1

Heavily inspired by Artyom's answer and combined with Unpack (only available in pyright atm, not yet in mypy), this is the best that can be done I think. But mostly I'll just use self.request("GET", ...) directly instead (because this solution still doesnt give proper code completion suggestions in vscode)

# mymodule.pyi

import requests
from typing import Any, Union
from typing_extensions import Unpack, TypedDict, NotRequired

class RequestArgs(TypedDict):
    name: NotRequired[str]
    params: NotRequired[Any]
    data: NotRequired[Any]
    headers: NotRequired[Any]
    cookies: NotRequired[Any]
    files: NotRequired[Any]
    auth: NotRequired[Any]
    timeout: NotRequired[Any]
    allow_redirects: NotRequired[bool]
    proxies: NotRequired[Any]
    hooks: NotRequired[Any]
    stream: NotRequired[Any]
    verify: NotRequired[bool]
    cert: NotRequired[Any]
    json: NotRequired[Any]

class NamePrintingSession(requests.Session):
    def get(self, url: Union[str, bytes], **kwargs: Unpack[RequestArgs]): ...
    def options(self, url: Union[str, bytes], **kwargs: Unpack[RequestArgs]): ...
    def head(self, url: Union[str, bytes], **kwargs: Unpack[RequestArgs]): ...
    def post(self, url: Union[str, bytes], **kwargs: Unpack[RequestArgs]): ...
    def put(self, url: Union[str, bytes], **kwargs: Unpack[RequestArgs]): ...
    def patch(self, url: Union[str, bytes], **kwargs: Unpack[RequestArgs]): ...
    def delete(self, url: Union[str, bytes], **kwargs: Unpack[RequestArgs]): ...
Cyberwiz
  • 11,027
  • 3
  • 20
  • 40