Skip to content

Allow using modules as subtypes of protocols #5018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ilevkivskyi opened this issue May 10, 2018 · 15 comments · Fixed by #13513
Closed

Allow using modules as subtypes of protocols #5018

ilevkivskyi opened this issue May 10, 2018 · 15 comments · Fixed by #13513

Comments

@ilevkivskyi
Copy link
Member

There is an idea to allow modules be accepted where a protocol is expected. This pattern is sometimes used for configs and option management, for example:

# file default_config.py
timeout = 100
one_flag = True
other_flag = False

# file __main__.py
import default_config
from typing import Protocol

class Options(Protocol):
    timeout: int
    one_flag: bool
    other_flag: bool

def setup(options: Options) -> None:
    ...

setup(default_config)  # OK

This will allow better typing than just types.ModuleType and should be straightforward to implement. Are there any objections against this?

@gvanrossum
Copy link
Member

gvanrossum commented May 10, 2018 via email

@gvanrossum
Copy link
Member

gvanrossum commented May 10, 2018 via email

@JukkaL
Copy link
Collaborator

JukkaL commented May 10, 2018

Sounds good. We should perhaps explicitly specify how module-level functions would work, since they don't take a self argument. Example:

# m.py
def f(x: int) -> None: pass

# main.py
import m
from typing import Protocol

class P(Protocol):
    def f(self, x: int) -> None: ...

p: P = m  # Should be ok?

@ilevkivskyi
Copy link
Member Author

@JukkaL Yes, I think just dropping the self is what everyone would expect.

@shoyer
Copy link

shoyer commented Jun 21, 2018

Another use case would be interchanging the random module and random.Random objects. (Or numpy.random and numpy.random.RandomState.)

This is a nice way to make functions that either use the global RNG state or support overloading with explicit objects, e.g.,

import random
from typing import Protocol

class Random(Protocol):
    ...  # all methods from random.Random

def random_add(rng: Random = random):
    return rng.rand() + rng.rand()

@ilevkivskyi
Copy link
Member Author

ilevkivskyi commented Jun 21, 2018

This is a nice way to make functions that either use the global RNG state or support overloading with explicit objects, e.g.,

Yes, I think everyone is sold on this (there is already a PR to add this to the PEP 544), but we just didn't have time to implement this (even though it is pretty straightforward).

ilevkivskyi added a commit to python/peps that referenced this issue May 11, 2019
@jbcpollak
Copy link

looks like the PEP was merged two years ago, but this is still an open issue. Is there any progress on this?

@hjwp
Copy link

hjwp commented Jul 30, 2020

Is there any progress on this?

Please don't do this. You can see there's been no progress. Nagging only makes the (unpaid, volunteer) maintainers feel stressed and underappreciated. If you really need a feature, the best thing you can do is to dig into the codebase and open a PR.

@jbcpollak
Copy link

I understand the etiquette, but from looking at the messages its unclear what the current state is - conversation seems to have just stalled. Its also labeled 'needs discussion'. @ilevkivskyi mentioned its straight forward work, what needs to be done? is it something a newbie can do?

@ilevkivskyi
Copy link
Member Author

is it something a newbie can do?

Hm, I think this is not the best first issue.

@hjwp
Copy link

hjwp commented Jul 30, 2020

sorry for lecturing 😅

@JukkaL
Copy link
Collaborator

JukkaL commented Jul 31, 2020

Removed the "needs discussion" label since we would definitely want to have this. However, the design of the implementation will still need some discussion :-)

@seiyab
Copy link

seiyab commented Jan 26, 2021

pyright seems to have implemented this feature.
It holds self in arguments.
And also allows @staticmethod without self.
https://github.com/microsoft/pyright/blob/d385058fe6b2d6e06eed648a96911240661e2750/packages/pyright-internal/src/tests/samples/protocolModule2.py#L7-L16
microsoft/pyright@d385058

jugmac00 added a commit to jugmac00/lpcraft that referenced this issue Jan 7, 2022
The class and its methods are only implemented to work around a bug in
mypy:
python/mypy#5018
@theCapypara
Copy link

theCapypara commented Jun 24, 2022

Is there at least a workaround for this? It seems even classes (the classes itself not the instances!) can't be tested against protocols:

from typing import Protocol

class AProtocol(Protocol):
    a: str

class A:
    a = "a"

a_ex: AProtocol = A

results in

Incompatible types in assignment (expression has type "Type[A]", variable has type "AProtocol")

Or is there no way at the moment with mypy to check protocols for "non-instantiated" objects? I need this functionality to check some static generated code.

@hauntsaninja
Copy link
Collaborator

@theCapypara your issue is unrelated. Do

if TYPE_CHECKING:
    a_concrete: A
    a_proto: AProtocol = a_concrete

ilevkivskyi added a commit that referenced this issue Aug 27, 2022
Fixes #5018 
Fixes #5439
Fixes #10850 

The implementation is simple but not the most beautiful one. I simply add a new slot to the `Instance` class that represents content of the module. This new attribute is short lived (it is not serialized, and not even stored on variables etc., because we erase it in `copy_modified()`). We don't need to store it, because all the information we need is already available in `MypyFile` node. We just need the new attribute to communicate between the checker and `subtypes.py`.

Other possible alternatives like introducing new dedicated `ModuleType`, or passing the symbol tables to `subtypes.py` both look way to complicated. Another argument in favor of this new slot is it could be useful for other things, like `hasattr()` support and ad hoc callable attributes (btw I am already working on the former).

Note there is one important limitation: since we don't store the module information, we can't support module objects stored in nested positions, like `self.mods = (foo, bar)` and then `accepts_protocol(self.mods[0])`. We only support variables (name expressions) and direct instance, class, or module attributes (see tests). I think this will cover 99% of possible use-cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants