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

Typing for get_values_for_type is incorrect #5889

Closed
reaperhulk opened this issue Mar 3, 2021 · 8 comments · Fixed by #5900
Closed

Typing for get_values_for_type is incorrect #5889

reaperhulk opened this issue Mar 3, 2021 · 8 comments · Fixed by #5900

Comments

@reaperhulk
Copy link
Member

def get_values_for_type(
self, type: typing.Type[GeneralName]
) -> typing.List[GeneralName]:
# Return the value of each GeneralName, except for OtherName instances
# which we return directly because it has two important properties not
# just one value.
objs = (i for i in self if isinstance(i, type))
if type != OtherName:
objs = (i.value for i in objs)
return list(objs)

This is currently defined as returning a list of GeneralName objects, but it actually returns a list of strings or a list of OtherName objects (which is a type of GeneralName). However, typing.Union[typing.List[str], typing.List[OtherName]] causes mypy to get confused as well. We should figure out how to make this (and the other instances of this same signature in extensions.py) correct.

@reaperhulk reaperhulk added the bugs label Mar 3, 2021
@reaperhulk reaperhulk added this to the Thirty Fifth Release milestone Mar 3, 2021
@reaperhulk
Copy link
Member Author

Actually it's worse; GeneralName values can be the following:

  • str
  • Name
  • ObjectIdentifier
  • IPv4Address/Network, IPv6Address/Network

And of course this method also returns a list of OtherName objects as well.

@reaperhulk
Copy link
Member Author

@mathiasertl do you have any ideas on sane typing here?

@mathiasertl
Copy link
Contributor

mathiasertl commented Mar 6, 2021

First, let me note that it took me 10 minutes to find out why the function was not just returning a list of GeneralName instances. Of course I'm just one data point, but for me the function code was hard to read.

But of course, you're right about the bug. The situation is worse yet, because off course the same is also true for SubjectAlternativeName, IssuerAlternativeName and CertificateIssuer. What I found out in my projects as well, is that functions that take/return lot's of different types are all great until you try to typehint them...

I see two possibilities right now (but will try to think of others too):

Option ONE: We typehint the return value to List[Union[str, Name, ...]]. The absolute downside is of course that mypy wont know about get_values_for_type(DNSName) returning only strings.

A side note here: I don't yet get why Typing.Union[typing.List[str], typing.List[OtherName]] causes troubles. We had that same issue in #5897. I will investigate that further.

Option TWO: We could try to overload the function (sorry @alex ;-)) to something like that (not tried it out yet, just pseudo code):

@overload
def get_values_for_type(self, type: x509.IPAddress) -> Union[ipaddress.IPv4Address, ...]:
    ...


@overload
def get_values_for_type(self, type: x509.RegisteredID) -> x509.ObjectIdentifier:
    ...

...

def get_values_for_type(self, type: NOTE-1-BELOW) -> Union[ipaddress.IPv4Address, ...]:
    ....

NOTE-1: Maybe we can just give x509.GeneralName here, we have to try that out.

I'm also not sure how/if overloading works if we give one of the stubs type: GeneralName) -> str as a "catch all" overload. We'll have to try that out too.

@alex
Copy link
Member

alex commented Mar 6, 2021 via email

@mathiasertl
Copy link
Contributor

mathiasertl commented Mar 6, 2021

There's also more underlying issues here, some we already discovered in other recent PRs:

mypy does not update the type of re-assigned variables. If I reveal_type() in the function as it is now:

        objs = (i for i in self if isinstance(i, type))
        reveal_type(objs)  # typing.Generator[GeneralName*, None, None]
        if type != OtherName:
            objs = (i.value for i in objs)
            reveal_type(objs)  # STILL typing.Generator[GeneralName*, None, None]

The solution here could be to simply return directly, instead. Update: That's why mypy didn't complain about return-typehint either.

I already tried a first (incomplete) type: Union[Type[OtherName], Type[DNSName], ...] and then mypy then doesn't get the type of the Generator anymore, so it's Generatory[Any, ...] after that. :-(

@mathiasertl
Copy link
Contributor

Alright, I have some good news and some bad news.

The good news: Using overloading as described above (of course with Type, as @alex mentioned), works well. With this python code (comments are line numbers):

import ipaddress
  
from cryptography import x509
from cryptography.x509.oid import ObjectIdentifier

names = x509.GeneralNames(
    [   
        x509.DNSName("example.com"),
        x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
    ]
)
reveal_type(names.get_values_for_type(x509.DNSName))  # 12
reveal_type(names.get_values_for_type(x509.UniformResourceIdentifier))  # 13
reveal_type(names.get_values_for_type(x509.RFC822Name))  # 14
reveal_type(names.get_values_for_type(x509.DirectoryName))  # 15
reveal_type(names.get_values_for_type(x509.RegisteredID))  # 16
reveal_type(names.get_values_for_type(x509.IPAddress))  # 17
reveal_type(names.get_values_for_type(x509.OtherName))  # 18

I get what I think we should get (right?):

test-5889.py:12: note: Revealed type is 'builtins.list[builtins.str]'
test-5889.py:13: note: Revealed type is 'builtins.list[builtins.str]'
test-5889.py:14: note: Revealed type is 'builtins.list[builtins.str]'
test-5889.py:15: note: Revealed type is 'builtins.list[cryptography.x509.name.Name]'
test-5889.py:16: note: Revealed type is 'builtins.list[cryptography.hazmat._oid.ObjectIdentifier]'
test-5889.py:17: note: Revealed type is 'builtins.list[Union[ipaddress.IPv4Address, ipaddress.IPv6Address, ipaddress.IPv4Network, ipaddress.IPv6Network]]'
test-5889.py:18: note: Revealed type is 'builtins.list[cryptography.x509.general_name.OtherName]'

The bad news: overloading is not inheritable, it seems. I tried moving this to a dedicated interface class, and it doesn't seem to work. I only found python/mypy#5146 which is going in that direction. In any case, I couldn't find a variant that does what it's supposed to do.

That means that the function declaration for get_values_for_type is a whopping 55 lines (without the actual implementation). And that is, once for every function that implements this.

You can view my changes here: https://github.com/mathiasertl/cryptography/tree/bugfix/issue-5889-typehint-get-values-for-types

@mathiasertl
Copy link
Contributor

So bottom line: If you're fine with those changes (in general), I'll make a PR of that branch to start the detailed review process.

@alex
Copy link
Member

alex commented Mar 7, 2021 via email

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 6, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Development

Successfully merging a pull request may close this issue.

3 participants