Repository: pyca/cryptography

Issue:
Typing for get_values_for_type is incorrect
https://github.com/pyca/cryptography/blob/032a7f3cdca5207d3b1e2a5eed14897d88f78378/src/cryptography/x509/extensions.py#L1392-L1401
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.


Discussion:
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.
@mathiasertl do you have any ideas on sane typing here?
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.
I think the overload approach is probably best. I'm pretty sure you'll need
to use `typing.Type[x509.IPAddress]` though.

On Sat, Mar 6, 2021 at 2:51 PM Mathias Ertl <notifications@github.com>
wrote:

> 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
> <https://github.com/pyca/cryptography/pull/5897>. I will investigate that
> further.
>
> Option TWO: We could try to overload the function (sorry @alex
> <https://github.com/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
> two.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/pyca/cryptography/issues/5889#issuecomment-792041592>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAAAGBBM2MDAJAFLHW6QN2TTCKBTPANCNFSM4YQQZBRA>
> .
>


-- 
All that is necessary for evil to succeed is for good people to do nothing.


Resolving patch:
diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py
--- a/src/cryptography/x509/extensions.py
+++ b/src/cryptography/x509/extensions.py
@@ -23,8 +23,18 @@
 from cryptography.x509.certificate_transparency import (
     SignedCertificateTimestamp,
 )
-from cryptography.x509.general_name import GeneralName, IPAddress, OtherName
-from cryptography.x509.name import RelativeDistinguishedName
+from cryptography.x509.general_name import (
+    DNSName,
+    DirectoryName,
+    GeneralName,
+    IPAddress,
+    OtherName,
+    RFC822Name,
+    RegisteredID,
+    UniformResourceIdentifier,
+    _IPADDRESS_TYPES,
+)
+from cryptography.x509.name import Name, RelativeDistinguishedName
 from cryptography.x509.oid import (
     CRLEntryExtensionOID,
     ExtensionOID,
@@ -1389,15 +1399,67 @@ def __init__(self, general_names: typing.Iterable[GeneralName]) -> None:
 
     __len__, __iter__, __getitem__ = _make_sequence_methods("_general_names")
 
+    @typing.overload
+    def get_values_for_type(
+        self,
+        type: typing.Union[
+            typing.Type[DNSName],
+            typing.Type[UniformResourceIdentifier],
+            typing.Type[RFC822Name],
+        ],
+    ) -> typing.List[str]:
+        ...
+
+    @typing.overload
     def get_values_for_type(
-        self, type: typing.Type[GeneralName]
-    ) -> typing.List[GeneralName]:
+        self,
+        type: typing.Type[DirectoryName],
+    ) -> typing.List[Name]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self,
+        type: typing.Type[RegisteredID],
+    ) -> typing.List[ObjectIdentifier]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self, type: typing.Type[IPAddress]
+    ) -> typing.List[_IPADDRESS_TYPES]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self, type: typing.Type[OtherName]
+    ) -> typing.List[OtherName]:
+        ...
+
+    def get_values_for_type(
+        self,
+        type: typing.Union[
+            typing.Type[DNSName],
+            typing.Type[DirectoryName],
+            typing.Type[IPAddress],
+            typing.Type[OtherName],
+            typing.Type[RFC822Name],
+            typing.Type[RegisteredID],
+            typing.Type[UniformResourceIdentifier],
+        ],
+    ) -> typing.Union[
+        typing.List[_IPADDRESS_TYPES],
+        typing.List[str],
+        typing.List[OtherName],
+        typing.List[Name],
+        typing.List[ObjectIdentifier],
+    ]:
         # 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 [i.value for i in objs]
         return list(objs)
 
     def __repr__(self) -> str:
@@ -1424,9 +1486,61 @@ def __init__(self, general_names: typing.Iterable[GeneralName]) -> None:
 
     __len__, __iter__, __getitem__ = _make_sequence_methods("_general_names")
 
+    @typing.overload
     def get_values_for_type(
-        self, type: typing.Type[GeneralName]
-    ) -> typing.List[GeneralName]:
+        self,
+        type: typing.Union[
+            typing.Type[DNSName],
+            typing.Type[UniformResourceIdentifier],
+            typing.Type[RFC822Name],
+        ],
+    ) -> typing.List[str]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self,
+        type: typing.Type[DirectoryName],
+    ) -> typing.List[Name]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self,
+        type: typing.Type[RegisteredID],
+    ) -> typing.List[ObjectIdentifier]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self, type: typing.Type[IPAddress]
+    ) -> typing.List[_IPADDRESS_TYPES]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self, type: typing.Type[OtherName]
+    ) -> typing.List[OtherName]:
+        ...
+
+    def get_values_for_type(
+        self,
+        type: typing.Union[
+            typing.Type[DNSName],
+            typing.Type[DirectoryName],
+            typing.Type[IPAddress],
+            typing.Type[OtherName],
+            typing.Type[RFC822Name],
+            typing.Type[RegisteredID],
+            typing.Type[UniformResourceIdentifier],
+        ],
+    ) -> typing.Union[
+        typing.List[_IPADDRESS_TYPES],
+        typing.List[str],
+        typing.List[OtherName],
+        typing.List[Name],
+        typing.List[ObjectIdentifier],
+    ]:
         return self._general_names.get_values_for_type(type)
 
     def __repr__(self) -> str:
@@ -1453,9 +1567,61 @@ def __init__(self, general_names: typing.Iterable[GeneralName]) -> None:
 
     __len__, __iter__, __getitem__ = _make_sequence_methods("_general_names")
 
+    @typing.overload
     def get_values_for_type(
-        self, type: typing.Type[GeneralName]
-    ) -> typing.List[GeneralName]:
+        self,
+        type: typing.Union[
+            typing.Type[DNSName],
+            typing.Type[UniformResourceIdentifier],
+            typing.Type[RFC822Name],
+        ],
+    ) -> typing.List[str]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self,
+        type: typing.Type[DirectoryName],
+    ) -> typing.List[Name]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self,
+        type: typing.Type[RegisteredID],
+    ) -> typing.List[ObjectIdentifier]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self, type: typing.Type[IPAddress]
+    ) -> typing.List[_IPADDRESS_TYPES]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self, type: typing.Type[OtherName]
+    ) -> typing.List[OtherName]:
+        ...
+
+    def get_values_for_type(
+        self,
+        type: typing.Union[
+            typing.Type[DNSName],
+            typing.Type[DirectoryName],
+            typing.Type[IPAddress],
+            typing.Type[OtherName],
+            typing.Type[RFC822Name],
+            typing.Type[RegisteredID],
+            typing.Type[UniformResourceIdentifier],
+        ],
+    ) -> typing.Union[
+        typing.List[_IPADDRESS_TYPES],
+        typing.List[str],
+        typing.List[OtherName],
+        typing.List[Name],
+        typing.List[ObjectIdentifier],
+    ]:
         return self._general_names.get_values_for_type(type)
 
     def __repr__(self) -> str:
@@ -1482,9 +1648,61 @@ def __init__(self, general_names: typing.Iterable[GeneralName]) -> None:
 
     __len__, __iter__, __getitem__ = _make_sequence_methods("_general_names")
 
+    @typing.overload
     def get_values_for_type(
-        self, type: typing.Type[GeneralName]
-    ) -> typing.List[GeneralName]:
+        self,
+        type: typing.Union[
+            typing.Type[DNSName],
+            typing.Type[UniformResourceIdentifier],
+            typing.Type[RFC822Name],
+        ],
+    ) -> typing.List[str]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self,
+        type: typing.Type[DirectoryName],
+    ) -> typing.List[Name]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self,
+        type: typing.Type[RegisteredID],
+    ) -> typing.List[ObjectIdentifier]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self, type: typing.Type[IPAddress]
+    ) -> typing.List[_IPADDRESS_TYPES]:
+        ...
+
+    @typing.overload
+    def get_values_for_type(
+        self, type: typing.Type[OtherName]
+    ) -> typing.List[OtherName]:
+        ...
+
+    def get_values_for_type(
+        self,
+        type: typing.Union[
+            typing.Type[DNSName],
+            typing.Type[DirectoryName],
+            typing.Type[IPAddress],
+            typing.Type[OtherName],
+            typing.Type[RFC822Name],
+            typing.Type[RegisteredID],
+            typing.Type[UniformResourceIdentifier],
+        ],
+    ) -> typing.Union[
+        typing.List[_IPADDRESS_TYPES],
+        typing.List[str],
+        typing.List[OtherName],
+        typing.List[Name],
+        typing.List[ObjectIdentifier],
+    ]:
         return self._general_names.get_values_for_type(type)
 
     def __repr__(self) -> str:
diff --git a/src/cryptography/x509/general_name.py b/src/cryptography/x509/general_name.py
--- a/src/cryptography/x509/general_name.py
+++ b/src/cryptography/x509/general_name.py
@@ -23,6 +23,12 @@
     7: "iPAddress",
     8: "registeredID",
 }
+_IPADDRESS_TYPES = typing.Union[
+    ipaddress.IPv4Address,
+    ipaddress.IPv6Address,
+    ipaddress.IPv4Network,
+    ipaddress.IPv6Network,
+]
 
 
 class UnsupportedGeneralNameType(Exception):
@@ -40,7 +46,7 @@ def value(self) -> typing.Any:
 
 
 class RFC822Name(GeneralName):
-    def __init__(self, value: str):
+    def __init__(self, value: str) -> None:
         if isinstance(value, str):
             try:
                 value.encode("ascii")
@@ -88,7 +94,7 @@ def __hash__(self) -> int:
 
 
 class DNSName(GeneralName):
-    def __init__(self, value: str):
+    def __init__(self, value: str) -> None:
         if isinstance(value, str):
             try:
                 value.encode("ascii")
@@ -130,7 +136,7 @@ def __hash__(self) -> int:
 
 
 class UniformResourceIdentifier(GeneralName):
-    def __init__(self, value: str):
+    def __init__(self, value: str) -> None:
         if isinstance(value, str):
             try:
                 value.encode("ascii")
@@ -174,7 +180,7 @@ def __hash__(self) -> int:
 
 
 class DirectoryName(GeneralName):
-    def __init__(self, value: Name):
+    def __init__(self, value: Name) -> None:
         if not isinstance(value, Name):
             raise TypeError("value must be a Name")
 
@@ -201,7 +207,7 @@ def __hash__(self) -> int:
 
 
 class RegisteredID(GeneralName):
-    def __init__(self, value: ObjectIdentifier):
+    def __init__(self, value: ObjectIdentifier) -> None:
         if not isinstance(value, ObjectIdentifier):
             raise TypeError("value must be an ObjectIdentifier")
 
@@ -228,15 +234,7 @@ def __hash__(self) -> int:
 
 
 class IPAddress(GeneralName):
-    def __init__(
-        self,
-        value: typing.Union[
-            ipaddress.IPv4Address,
-            ipaddress.IPv6Address,
-            ipaddress.IPv4Network,
-            ipaddress.IPv6Network,
-        ],
-    ):
+    def __init__(self, value: _IPADDRESS_TYPES) -> None:
         if not isinstance(
             value,
             (
@@ -255,14 +253,7 @@ def __init__(
         self._value = value
 
     @property
-    def value(
-        self,
-    ) -> typing.Union[
-        ipaddress.IPv4Address,
-        ipaddress.IPv6Address,
-        ipaddress.IPv4Network,
-        ipaddress.IPv6Network,
-    ]:
+    def value(self) -> _IPADDRESS_TYPES:
         return self._value
 
     def __repr__(self) -> str:
@@ -282,7 +273,7 @@ def __hash__(self) -> int:
 
 
 class OtherName(GeneralName):
-    def __init__(self, type_id: ObjectIdentifier, value: bytes):
+    def __init__(self, type_id: ObjectIdentifier, value: bytes) -> None:
         if not isinstance(type_id, ObjectIdentifier):
             raise TypeError("type_id must be an ObjectIdentifier")
         if not isinstance(value, bytes):
diff --git a/src/cryptography/x509/oid.py b/src/cryptography/x509/oid.py
--- a/src/cryptography/x509/oid.py
+++ b/src/cryptography/x509/oid.py
@@ -265,3 +265,18 @@ class AttributeOID(object):
     OCSPExtensionOID.NONCE: "OCSPNonce",
     AttributeOID.CHALLENGE_PASSWORD: "challengePassword",
 }
+
+
+__all__ = [
+    "AttributeOID",
+    "AuthorityInformationAccessOID",
+    "CRLEntryExtensionOID",
+    "CertificatePoliciesOID",
+    "ExtendedKeyUsageOID",
+    "ExtensionOID",
+    "NameOID",
+    "OCSPExtensionOID",
+    "ObjectIdentifier",
+    "SignatureAlgorithmOID",
+    "SubjectInformationAccessOID",
+]


Test patch: