-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Add stubs for python-decouple #10743
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
base: main
Are you sure you want to change the base?
Conversation
stubs/python-decouple/decouple.pyi
Outdated
| class _CsvType(Protocol[_T, _TCsv]): | ||
| cast: Callable[..., _T] | ||
| delimiter: str | ||
| strip: str | ||
| post_process: Callable[..., _TCsv] | ||
| def __call__(self, value: str) -> _TCsv: ... | ||
|
|
||
| @overload | ||
| def Csv() -> _CsvType[str, list[str]]: ... | ||
| @overload | ||
| def Csv(cast: Callable[..., _T], delimter: str = ..., strip: str = ...) -> _CsvType[_T, list[_T]]: ... | ||
| @overload | ||
| def Csv(cast: Callable[..., _T], delimiter: str, strip: str, post_process: Callable[..., _TCsv]) -> _CsvType[_T, _TCsv]: ... | ||
| @overload | ||
| def Csv( | ||
| *, cast: Callable[..., _T], post_process: Callable[..., _TCsv], delimiter: str = ..., strip: str = ... | ||
| ) -> _CsvType[_T, _TCsv]: ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is likely the most complex use case in this module. The Csv class exhibits default behavior that can be modified based on parameters passed into its __init__ method. By default, cast is set to str, and post_process is set to list. Therefore, if these parameters are explicitly defined, the types provided should be used. However, if they are not specified, the return type of the __call__ method should default to list[str].
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is a better way to express this, I am open to suggestions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't looked deeply, but it sounds like you simply need a generic __init__ method.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
stubs/python-decouple/decouple.pyi
Outdated
| def __init__( | ||
| self, cast: Callable[..., _T] = str, delimiter: str = ..., strip: str = ..., post_process: Callable[..., _TCsv] = list[_T] | ||
| ) -> None: ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| def __init__( | |
| self, cast: Callable[..., _T] = str, delimiter: str = ..., strip: str = ..., post_process: Callable[..., _TCsv] = list[_T] | |
| ) -> None: ... | |
| @overload | |
| def __init__( | |
| self: Csv[str, list[str]], cast: Callable[[str], str] = ..., delimiter: str = ",", strip: str = ..., post_process: Callable[[Iterable[str]], list[str]] = ... | |
| ) -> None: ... | |
| @overload | |
| def __init__( | |
| self: Csv[_T, list[_T]], cast: Callable[[str], _T], delimiter: str = ",", strip: str = ..., post_process: Callable[[Iterable[_T]], list[_T]] = ... | |
| ) -> None: ... | |
| ) -> None: ... | |
| @overload | |
| def __init__( | |
| self: Csv[str, _TCsv], cast: Callable[[str], str] = ..., delimiter: str = ",", strip: str = ..., *, post_process: Callable[Iterable[str], _TCsv] | |
| ) -> None: ... | |
| @overload | |
| def __init__( | |
| self: Csv[_T, _TCsv], cast: Callable[[str], _T], delimiter: str, strip: str, post_process: Callable[[Iterable[_T]], _TCsv] |
I think this should be something along these lines. The arguments of cast/post_process are very much defined by the implementation and since they are optional parameters you will need to provide overloads, because otherwise _T/_TCsv will not get bound.
It's also worth noting that you will need to import Iterable to make this suggestion work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this suggestion! I did not realize you could suggest the types of _T and _Csv through the self argument. Really interesting stuff.
After I implemented your suggestions, I am getting some regressions on my code while using this type file. Would you know how to fix it? Also, I am wondering if the Iterable is having some unwanted side effects. Also, the errors I am seeing are coming from mypy, I haven't checked the other type checkers.
from decouple import Choices, Csv
from typing_extensions import reveal_type
# No error and correct type
reveal_type(Csv()("")) # note: Revealed type is "builtins.list[builtins.str]"
# No error and correct type
reveal_type(Csv(cast=int)("")) # note: Revealed type is "builtins.list[builtins.int]"
# error: Argument "post_process" to "Csv" has incompatible type "type[set[Any]]"; expected "Callable[[Iterable[str]], list[str]]" [arg-type]
reveal_type(Csv(post_process=set)("")) # note: Revealed type is "builtins.list[builtins.str]"
# error: Argument 1 to "Csv" has incompatible type "type[int]"; expected "Callable[[str], str]" [arg-type]
# error: Argument 4 to "Csv" has incompatible type "type[tuple[Any, ...]]"; expected "Callable[[Iterable[str]], list[str]]" [arg-type]#
reveal_type(Csv(int, ",", " ", tuple)(""))
# error: Argument "cast" to "Csv" has incompatible type "type[int]"; expected "Callable[[str], str]" [arg-type]
# error: Argument "post_process" to "Csv" has incompatible type "type[tuple[Any, ...]]"; expected "Callable[[Iterable[str]], list[str]]" [arg-type]
reveal_type(Csv(cast=int, post_process=tuple)(""))
# error: Argument "cast" to "Csv" has incompatible type "type[int]"; expected "Callable[[str], str]" [arg-type]
# error: Argument "post_process" to "Csv" has incompatible type "type[tuple[Any, ...]]"; expected "Callable[[Iterable[str]], list[str]]" [arg-type]
reveal_type(Csv(cast=int, delimiter=",", post_process=tuple)(""))There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I am not surprised there are some cases where mypy can't infer this correctly, since there's quite some complex matching logic involved and the one type var technically depends on the other, however there is no real way to express this relationship between the two, it would require higher kinded type vars.
The main complication stems from using callables like set, tuple as arguments for post_process, since they are generics in themselves, if you use callables with fixed arguments, then it will work no problem.
I've played around a bit on mypy-play, it needed one more overload to cover all the cases correctly, I've also tried a callback protocol, but that just resulted in very strange behavior of inferring Csv(post_process=set) as Csv[Any, list[Any]] which is very incorrect.
This is the closest I could get, it's not perfect, since it results in unbound type vars for your examples, so if you try to use the result of __call__ mypy will tell you to specify the type, since it can't fully infer it itself. I've also had to give up on some amount of type safety for post_process and define the input as Iterable[Any] rather than Iterable[_T].
https://mypy-play.net/?mypy=latest&python=3.11&gist=8aace8121128e574dae382a18dd3fc02
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main complication stems from using callables like set, tuple as arguments for post_process, since they are generics in themselves, if you use callables with fixed arguments, then it will work no problem.
I was trying to work through that as well, and I have come to same conclusion. However, at least there is a solution.
This is the closest I could get, it's not perfect, since it results in unbound type vars for your examples, so if you try to use the result of call mypy will tell you to specify the type, since it can't fully infer it itself. I've also had to give up on some amount of type safety for post_process and define the input as Iterable[Any] rather than Iterable[_T]. https://mypy-play.net/?mypy=latest&python=3.11&gist=8aace8121128e574dae382a18dd3fc02
I think I have incorporated all of your updates into the PR. Thanks for the suggestions.
stubs/python-decouple/decouple.pyi
Outdated
| from typing import Any, Generic, TextIO, TypeVar, overload | ||
|
|
||
| _T = TypeVar("_T") | ||
| _TCsv = TypeVar("_TCsv", bound=Sequence[Any]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this upper bound is too strict, I would change this to Collection[Any] at the very least, but technically this can be anything and the docs also state as much (you could e.g. imagine someone calculating an integer hash from the individual values by providing post_process=lambda x: hash(tuple(x))) so I'm not sure if it's worth providing an upper bound.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that's a great point! Updated!
stubs/python-decouple/decouple.pyi
Outdated
| cast: Callable[..., _T] | ||
| choices: Sequence[_T] | ||
| def __init__( | ||
| self, | ||
| flat: Sequence[_T] | None = None, | ||
| cast: Callable[..., _T] = ..., | ||
| choices: Sequence[_T | tuple[Incomplete, _T]] | None = None, | ||
| ) -> None: ... | ||
| def __call__(self, value: str) -> _T: ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| cast: Callable[..., _T] | |
| choices: Sequence[_T] | |
| def __init__( | |
| self, | |
| flat: Sequence[_T] | None = None, | |
| cast: Callable[..., _T] = ..., | |
| choices: Sequence[_T | tuple[Incomplete, _T]] | None = None, | |
| ) -> None: ... | |
| def __call__(self, value: str) -> _T: ... | |
| cast: Callable[[str], _T] | |
| choices: Sequence[tuple[_T, str]] | |
| @overload | |
| def __init__( | |
| self: Choices[str], | |
| flat: Sequence[str] | None = None, | |
| cast: Callable[[str], str] = ..., | |
| choices: Sequence[tuple[str, str]] | None = None, | |
| ) -> None: ... | |
| @overload | |
| def __init__( | |
| self: Choices[_T], | |
| flat: Sequence[_T], | |
| cast: Callable[[str], _T] = ..., | |
| choices: Sequence[tuple[_T, str]] | None = None, | |
| ) -> None: ... | |
| @overload | |
| def __init__( | |
| self: Choices[_T], | |
| flat: None, | |
| cast: Callable[[str], _T], | |
| choices: Sequence[tuple[_T, str]] | None = None, | |
| ) -> None: ... | |
| @overload | |
| def __init__( | |
| self: Choices[_T], | |
| flat: None = None, | |
| cast: Callable[[str], _T] = ..., | |
| *, | |
| choices: Sequence[tuple[_T, str]], | |
| ) -> None: ... | |
| def __call__(self, value: str) -> _T: ... |
Same deal here, this takes some overloads to bind correctly and the parameters of cast are given, and choices does need to be a sequence of 2 value tuples, the second value of which is always a string.
Technically you could make this generic in the input value type as well, but since it is meant for dealing with environment variables, it probably makes sense to stick with str as all the examples have as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great! Thanks again for the suggestion.
I have one issue, it seems like when cast is used by itself mypy seems to get confused. Would you know of a way to fix that use case?
from decouple import Choices, Csv
from typing_extensions import reveal_type
# error: Argument "cast" to "Choices" has incompatible type "type[int]"; expected "Callable[[str], str]" [arg-type]
reveal_type(Choices(cast=int)(""))
reveal_type(Choices(flat=[1, 2])("")) # note: Revealed type is "builtins.int"
reveal_type(Choices(choices=[(1, "one"), (2, "two")])("")) #note: Revealed type is "builtins.int"
reveal_type(Choices(cast=int, flat=[1, 2])("")) #note: Revealed type is "builtins.int"
reveal_type(Choices(cast=int, choices=[(1, "one"), (2, "two")])("")) #note: Revealed type is "builtins.int"
reveal_type(Choices(flat=[1, 2], choices=[(1, "one"), (2, "two")])("")) #note: Revealed type is "builtins.int"
reveal_type(Choices(cast=int, flat=[1, 2], choices=[(1, "one"), (2, "two")])("")) #note: Revealed type is "builtins.int"There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs one additional overload to cover that case:
@overload
def __init__(
self: Choices[_T],
flat: Sequence[_T] | None = None,
*,
cast: Callable[[str], _T],
choices: Sequence[tuple[_T, str]] | None = None,
) -> None: ...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generic overload matching gets complicated really quickly if you want to cover every possible case on arguments other than the first one or two...
I wish overloads would let you violate the requirement of positional optional parameters having to come after all the required ones, it would get rid of a lot of redundant overloads, that have to currently be used in order to express both the positional and keyword version of the overload.
But that would of course require some changes to the language, since the parser would have to ignore these violations specifically only on overloads, the alternative would be to have some typing specific sentinel value that means the parameter is required in order to match that overload, but the parser can still treat it like an optional one. But I am getting off course here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generic overload matching gets complicated really quickly if you want to cover every possible case on arguments other than the first one or two...
Yeah, I know what you mean.
I wish overloads would let you violate the requirement of positional optional parameters having to come after all the required ones, it would get rid of a lot of redundant overloads, that have to currently be used in order to express both the positional and keyword version of the overload.
But that would of course require some changes to the language, since the parser would have to ignore these violations specifically only on overloads, the alternative would be to have some typing specific sentinel value that means the parameter is required in order to match that overload, but the parser can still treat it like an optional one. But I am getting off course here.
TBH, this is how I thought it would work but the more I dug into making this type module, the more I realized that wasn't the case. But, now I know and thanks for the background!
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
I realized that Config doesn't really care if repository inherits from RepsositoryEmpty or not, but it just cares if it has `__contain__` and `__getitem__`
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Also remove type safety on post_process, since it could be `_T` or it could be `str`
This comment has been minimized.
This comment has been minimized.
stubs/python-decouple/decouple.pyi
Outdated
| cast: Callable[[str], _T] | ||
| delimiter: str | ||
| strip: str | ||
| post_process: Callable[[Iterable[Any]], _TCsv] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the Any be _T as the post-processor gets passed whatever came out of cast?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that was my first suggestion, and we tried that, however mypy will then reject generic classes as a post_process since it can't seem to solve for both type vars simultaneously i.e. it will reject post_process=set but it would accept post_process=set[str].
I agree that this should work, but it's also a very challenging scenario for an overload solver, so I am not surprised mypy can't do it yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I add a comment that "it should be _T but mypy wasn't able to support it." So in the future, it's more obvious about the decision?
stubs/python-decouple/decouple.pyi
Outdated
| delimiter: str | ||
| strip: str | ||
| post_process: Callable[[Iterable[Any]], _TCsv] | ||
| def __call__(self, value: str) -> _TCsv: ... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation also supports passing None. In that case, post_process is called with no arguments. Typing that accurately would probably require making a callback protocol for post_process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, I glossed over that part in my review. I am however a little concerned how well a callback protocol will behave, considering mypy would match Csv(post_process=set) to Csv[Any, builtins.list[Any]] when I tried to replace it with a callback protocol. Changing the order of overloads, so the first one is fully generic, would probably solve that, but you would still end up with unsolved type vars. Although I am not sure I have tried a callback protocol with Any for the first argument, instead of _T, so maybe that will behave a bit better.
stubs/python-decouple/decouple.pyi
Outdated
| class Choices(Generic[_T]): | ||
| flat: Sequence[_T] | ||
| cast: Callable[[str], _T] | ||
| choices: Sequence[_T] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be Sequence[tuple[_T, object]]?
Also in the overloads below I think the second tuple member in choices should be object, as the implementation ignores it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, I had already suggested the same change for choices but I guess it fell victim to manual copy pasta.
While I do agree that the second member in the tuple is ignored and could be anything, I don't think it's a good change.
It expects that when cast is used on the second element in the tuple, for it to return the first element, so while the implementation is bogus and backwards and doesn't verify that, the stubs should probably reflect the intent and the intent was for the second element to be the string representation of the choice.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
@JelleZijlstra Since you've already fixed some problem, do you want to close the remaining problems ( |
|
According to mypy_primer, this change has no effect on the checked open source code. 🤖🎉 |
This is a small single file module which for the most has a main
configfunction and a few supporting classes.I used the docs as a first reference, since they contain some type hints and then verified it with the actual implementation.
The doc is available here: https://github.com/HBNetwork/python-decouple/blob/master/README.rst
Source code here: https://github.com/HBNetwork/python-decouple/blob/master/decouple.py