Skip to content
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

Protocol checking against typing_extenstion.Protocol fails #479

Closed
2 tasks done
iliapolo opened this issue Aug 27, 2024 · 9 comments
Closed
2 tasks done

Protocol checking against typing_extenstion.Protocol fails #479

iliapolo opened this issue Aug 27, 2024 · 9 comments
Labels

Comments

@iliapolo
Copy link

Things to check first

  • I have searched the existing issues and didn't find my bug already reported there

  • I have checked that my bug is still present in the latest release

Typeguard version

4.3.0

Python version

Python 3.10.4

What happened?

Expected typeguard to ignore private attributes of the typing_extension.Protocol so that type checking would pass.

Note: Honestly this was my first intuition, but I realize it might be out of scope as I'm not sure what your stance is with respect to supporting the typing_extensions package. Perhaps this issue is more a feature request to allow configuring custom attributes that typeguard will ignore when typechecking protocols.

How can we reproduce the bug?

import typing_extensions
import builtins
import typeguard

class IDo(typing_extensions.Protocol):
    def do_something(self) -> builtins.str:
        ...

def doer(do: IDo) -> builtins.str:
    typeguard.check_type(value=do, expected_type=IDo)
    return do.do_something()

class DefaultDo:
    def do_something(self) -> builtins.str:
        return "did something"
    
print(doer(DefaultDo()))

Running the above program will result in:

typeguard.TypeCheckError: __main__.DefaultDo is not compatible with the IDo protocol because it has no attribute named '__protocol_attrs__'

Indeed, extending the typing_extensions.Protocol adds the __protocol_attrs__ attribute and causes the mismatch. I did notice however that typeguard has provisions to ignore private attributes of the typing.Protocol class:

ignored_attrs = set(dir(typing.Protocol)) | {
"__annotations__",
"__non_callable_proto_members__",
}

So I was thinking this might be extended to support typing_extension.Protocol? If not, it would be great if we had some way to inject custom attributes we'd like typeguard to ignore.

@iliapolo iliapolo added the bug label Aug 27, 2024
@iliapolo iliapolo changed the title Incorrect Protocol checking against typing_extenstion.Protocol fails Aug 27, 2024
@agronholm
Copy link
Owner

Does Mypy pass your code in strict mode?

@iliapolo
Copy link
Author

Looks like it:

mypy --version                                                                                                                                                                                                                                    [12:51:41]
mypy 1.11.2 (compiled: yes)mypy --strict tests/exp.py                                                                                                                                                                                                                        [12:51:45]
Success: no issues found in 1 source file

@agronholm
Copy link
Owner

Yeah, I recall something like this was reported before. I'll get it fixed for the next release, but I'm currently busy trying to make new releases in two other projects. I'll tackle typeguard after that.

@iliapolo
Copy link
Author

iliapolo commented Aug 27, 2024

@agronholm If we agree on an approach here i'm happy to contribute a PR. Would the solution be using dir(typing_extensions.Protocol) instead of dir(typing.Protocol)? maybe a union of both?

@thetorpedodog
Copy link
Contributor

This is not specific to typing_extensions.Protocol. Take, for instance, these two completely empty protocols, which should accept all objects:

# typedmod.py

import typing as t

T_co = t.TypeVar("T_co", covariant=True)

class Empty(t.Protocol):
    pass

class GenericEmpty(t.Protocol[T_co]):
    pass


def takes_empty(it: Empty) -> None:
    pass

def takes_generic_empty(it: GenericEmpty[T_co]) -> None:
    pass

When using them in a terminal session:

>>> import typeguard
>>> typeguard.install_import_hook()
<typeguard.ImportHookManager object at 0x7fc334c03d10>
>>> import typedmod
>>> typedmod.takes_empty("")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../typedmod.py", line 12, in takes_empty
    def takes_empty(it: Empty) -> None:
  File ".../venv/tg-playground/lib/python3.12/site-packages/typeguard/_functions.py", line 136, in check_argument_types
    check_type_internal(value, annotation, memo)
  File ".../venv/tg-playground/lib/python3.12/site-packages/typeguard/_checkers.py", line 861, in check_type_internal
    checker(value, origin_type, args, memo)
  File ".../venv/tg-playground/lib/python3.12/site-packages/typeguard/_checkers.py", line 738, in check_protocol
    raise TypeCheckError(
typeguard.TypeCheckError: argument "it" (str) is not compatible with the Empty protocol because it has no attribute named '__weakref__'
>>> typedmod.takes_generic_empty("")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../typedmod.py", line 15, in takes_generic_empty
    def takes_generic_empty(it: GenericEmpty[T_co]) -> None:
  File ".../venv/tg-playground/lib/python3.12/site-packages/typeguard/_functions.py", line 136, in check_argument_types
    check_type_internal(value, annotation, memo)
  File ".../venv/tg-playground/lib/python3.12/site-packages/typeguard/_checkers.py", line 861, in check_type_internal
    checker(value, origin_type, args, memo)
  File ".../venv/tg-playground/lib/python3.12/site-packages/typeguard/_checkers.py", line 738, in check_protocol
    raise TypeCheckError(
typeguard.TypeCheckError: argument "it" (str) is not compatible with the GenericEmpty protocol because it has no attribute named '__orig_bases__'

What this suggests is that you could use the attributes of an empty protocol (like GenericEmpty above) to generate the list of things you need to ignore when checking a protocol.

@thetorpedodog
Copy link
Contributor

What this suggests is that you could use the attributes of an empty protocol (like GenericEmpty above) to generate the list of things you need to ignore when checking a protocol.

Actually, upon further research, the solution might be simpler: use typing_extensions.get_protocol_members (to become typing.get_protocol_members in 3.13). If you don’t want to have a hard dependency on typing_extensions, this might require you to vendor the relevant functions.

@agronholm
Copy link
Owner

Actually, upon further research, the solution might be simpler: use typing_extensions.get_protocol_members (to become typing.get_protocol_members in 3.13). If you don’t want to have a hard dependency on typing_extensions, this might require you to vendor the relevant functions.

Typeguard already has a hard dependency on typing_extensions, so I'll just use that function then. Thanks!

@agronholm
Copy link
Owner

This will be fixed with #490.

@agronholm
Copy link
Owner

This was fixed in v4.4.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants