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
Fix #303: Fix Version.__init__ method #317
base: master
Are you sure you want to change the base?
Fix #303: Fix Version.__init__ method #317
Conversation
8612834
to
1587fa0
Compare
b66fc51
to
c22f5a2
Compare
@@ -144,27 +162,122 @@ class Version: | |||
|
|||
def __init__( |
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.
Maybe add some overloads for more clarity? Or we can resolve this in another issue
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.
Ahh, you mean something like this?
from typing import overload
@overload
def __init__(self, ): # for Version("1.2.3", ...)
...
@overload
def __init__(self, ): # for Version(1, 2, 3)
...
@overload
def __init__(self, ): # for Version(
# the real implementation
Unfortunately, I don't know how these type annotations should look like. Types are new topic for me, but I like the idea. Any hints for me? 😉
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.
Well the overloads simply specify which signatures are valid. For example, if the version is given as a string, assuming the string has all the information we need, we don't have to (and we shouldn't) pass any of the major, minor, patch, etc to the function. In our case, the type hint without overload would look like this: (version_string_or_major: Union[str, SupportInt], minor: SupportsInt = None, ...)
which is ugly and is totally useless to the user of the function.
Now with overload it would look like this:
from typing import overload
@overload
def __init__(self, version_string: str) -> Version: # for Version("1.2.3", ...)
...
@overload
def __init__(self, major: SupportsInt, minor: SupportsInt, patch: SupportsInt) -> Version: # for Version(1, 2, 3)
...
# We should do something about the kwargs like `prerelease` and `build`.
# Probably best to throw them in the implementation function signature.
# Do not put @overload here, because this is the actual implementation.
def __init__(self, *args, prerelease: String = None, build: String = None, **kwargs) -> Version: # for Version(
# the real implementation
And now we can call the function in each way:
Version("1.2.4") # correct
Version(1, 2, 4) # correct
Version("1.2.4", 3, 2) # type error (we should probably also raise a NotImplementedError at runtime)
Now you could chuck that code in there, but I still have doubts about how mypy will handle the prerelease
and build
kwargs in relation to the three main parts of the version. This would require further testing and experimentation. For now, I think it lays the foundation, and we can add more overloads as we find out what we can do correctly with this method.
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.
Ok, I've tried this and added your code. However, I get this from mypy
:
def __init__(self, version_string: str) -> Version:
NameError: name 'Version' is not defined
I guess, __init__
is special here as it does NOT return any values. So I removed the -> Version
part and tried it again. That was better, but I get this from mypy
:
src/semver/version.py:122: error: Overloaded function implementation does not accept all possible arguments of signature 1
src/semver/version.py:122: error: Overloaded function implementation does not accept all possible arguments of signature 2
src/semver/version.py:382: error: No overload variant of "Version" matches argument type "int"
src/semver/version.py:382: note: Possible overload variants:
src/semver/version.py:382: note: def __init__(self, version_string: str) -> Version
src/semver/version.py:382: note: def __init__(self, major: SupportsInt, minor: SupportsInt, patch: SupportsInt) -> Version
src/semver/version.py:395: error: No overload variant of "Version" matches argument types "int", "int"
src/semver/version.py:395: note: Possible overload variants:
src/semver/version.py:395: note: def __init__(self, version_string: str) -> Version
src/semver/version.py:395: note: def __init__(self, major: SupportsInt, minor: SupportsInt, patch: SupportsInt) -> Version
src/semver/version.py:423: error: No overload variant of "Version" matches argument types "int", "int", "int", "str"
src/semver/version.py:423: note: Possible overload variants:
src/semver/version.py:423: note: def __init__(self, version_string: str) -> Version
src/semver/version.py:423: note: def __init__(self, major: SupportsInt, minor: SupportsInt, patch: SupportsInt) -> Version
src/semver/version.py:443: error: No overload variant of "Version" matches argument types "int", "int", "int", "Optional[str]", "str"
src/semver/version.py:443: note: Possible overload variants:
src/semver/version.py:443: note: def __init__(self, version_string: str) -> Version
src/semver/version.py:443: note: def __init__(self, major: SupportsInt, minor: SupportsInt, patch: SupportsInt) -> Version
src/semver/_deprecated.py:240: error: No overload variant of "Version" matches argument types "Any", "Any", "Any", "Any", "Any"
src/semver/_deprecated.py:240: note: Possible overload variants:
src/semver/_deprecated.py:240: note: def __init__(self, version_string: str) -> Version
src/semver/_deprecated.py:240: note: def __init__(self, major: SupportsInt, minor: SupportsInt, patch: SupportsInt) -> Version
Found 7 errors in 2 files (checked 7 source files)
It seems, it's difficult to use positional arguments and keyword arguments at the same time as https://stackoverflow.com/a/57225493 implies.
Any ideas?
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.
For the Version
type hints, just put quotes around it. This is because it is not declared yet and the evaluation needs to be deferred when performing the type checks. I think in python 3.10 all type hints evaluation will be deferred by default, but for now we've got to use a string for that effect.
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.
For the overloads with the kwargs, I think adding more overloads for each possible combination of prerelease
and build
would help. Or maybe we can try something like **kwargs: String
after the three positional parts.
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.
For the
Version
type hints, just put quotes around it.
I've tried that before. 😉 This is not possible as you get from mypy
:
error: The return type of "__init__" must be None
In other words: the __init__
function does not have any return type at all (except 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.
No idea how to solve that. 🤷♂️ Even if I add no return types to __init__
I still get the above errors ("No overload variant of "Version" matches argument types...").
self._major = version_parts["major"] | ||
self._minor = version_parts["minor"] | ||
self._patch = version_parts["patch"] | ||
self._prerelease = None if prerelease is None else str(prerelease) | ||
self._build = None if build is None else str(build) | ||
|
||
@classmethod | ||
def _ensure_str(cls, s: Optional[String], encoding="UTF-8") -> Optional[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.
def _ensure_str(cls, s: Optional[String], encoding="UTF-8") -> Optional[str]: | |
def _ensure_str(cls, s: Optional[StringOrInt], encoding="UTF-8") -> Optional[str]: |
This should resolve type issues you might be having earlier when using that method.
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.
Thanks. I had to change the return type as well (to Optional[StringOrInt]
), otherwise I get the error message:
src/semver/version.py:228: error: Incompatible return value type (got "Union[str, int, None]", expected "Optional[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.
Make sure you use Optional[str]
here, since it is meant to ensure that it is a str
.
I think we may have an architecture problem on our hands here. We shouldn't be calling _ensure_str
on things other than a variation of string, so we ought to make sure we aren't passing in an int
.
Also, please ignore the first code suggestion, that kind of made it wrong 🤷♂️
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 we may have an architecture problem on our hands here. We shouldn't be calling
_ensure_str
on things other than a variation of string, so we ought to make sure we aren't passing in anint
.
Yes, could be. That's why I didn't merge this PR yet. It is a bigger change and I would like to be fully sure and discuss that with you first. I wouldn't like to introduce any regression. 😉
Well, I thought, it could be an elegant way to have this line:
prerelease = cls._ensure_str(prerelease or verlist[3])
That would either convert bytes -> str or left None and int untouched. Further down this case is handled:
self._prerelease = None if prerelease is None else str(prerelease)
So here is the final assignment to self._prerelease
to either store None or a string (regardless if it was passed as bytes, str, or int).
Maybe the name of the function is a bit irritating. Or we need to find a better way to assign self._prerelease
its final value. Any ideas?
08388a0
to
6f64b71
Compare
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.
My bad for leading you astray in the last review, this should be more correct now
14e4f96
to
93eebfe
Compare
@tlaferriere Sorry for the delay, I was busy with other things. I've played around a bit and introduced another function. I'm not completely happy, but the if clauses are kind of hidden inside a new Apart from this, I'm not sure about the Regarding the Thoughts? 🙂 |
f9ef14f
to
edf398b
Compare
66af43c
to
ff470fa
Compare
* Allow different variants to call Version * Adapt the documentation and README * Adapt and amend tests * Add changelog entries * Add function "remove_noqa" in conf.py to remove any "# noqa" lines for flake8 issues like overly long lines * Introduce a (private) _ensure_str class method
Co-authored-by: Thomas Laferriere <t.laferriere@hotmail.ca>
Used to catch integer values, but delegate anything else to _ensure_str
* Introduce new class variables VERSIONPARTS, VERSIONPARTDEFAULTS, and ALLOWED_TYPES * Simplify __init__; outsource some functionality like type checking into different functions * Use dict merging between *args and version components
282e9c7
to
26ab6d0
Compare
This PR fixes #303 and contains the following changes:
TODO: Fix typing errors.@tlaferriere Thomas, could you have a look please? Especially the types, they seem to be a bit tricky. 😉
I deviated a bit from the original signature and used the major, minor, etc. keyword arguments instead of kwargs:
The tests are okay, butmypy
complains.