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

Fix #303: Fix Version.__init__ method #317

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

tomschr
Copy link
Member

@tomschr tomschr commented Nov 6, 2020

This PR fixes #303 and contains the following changes:

  • Allow different variants to call Version
  • Adapt the documentation and README
  • Adapt and amend tests
  • Add changelog entries

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:

def __init__(
        self,
        *args: Tuple,  # type needs to be fixed probably
        major: SupportsInt = 0,
        minor: SupportsInt = 0,
        patch: SupportsInt = 0,
        prerelease: StringOrInt = None,
        build: StringOrInt = None,
    ):

The tests are okay, but mypy complains.

@tomschr tomschr added Release_3.x.y Only for the major release 3 Enhancement Not a bug, but increases or improves in value, quality, desirability, or attractiveness labels Nov 6, 2020
@tomschr tomschr self-assigned this Nov 6, 2020
@tomschr tomschr requested a review from a team November 6, 2020 20:43
@tomschr tomschr force-pushed the feature/303-refactor-__init__ branch 2 times, most recently from 8612834 to 1587fa0 Compare November 8, 2020 14:46
@tomschr tomschr marked this pull request as ready for review November 8, 2020 14:50
@tomschr tomschr force-pushed the feature/303-refactor-__init__ branch 7 times, most recently from b66fc51 to c22f5a2 Compare November 8, 2020 19:38
@@ -144,27 +162,122 @@ class Version:

def __init__(
Copy link
Contributor

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

Copy link
Member Author

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? 😉

Copy link
Contributor

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.

Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Member Author

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).

Copy link
Member Author

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]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Copy link
Member Author

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]")

Copy link
Contributor

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 🤷‍♂️

Copy link
Member Author

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 an int.

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?

@tomschr tomschr force-pushed the feature/303-refactor-__init__ branch 3 times, most recently from 08388a0 to 6f64b71 Compare November 12, 2020 08:31
Copy link
Contributor

@tlaferriere tlaferriere left a 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

src/semver/version.py Outdated Show resolved Hide resolved
src/semver/version.py Outdated Show resolved Hide resolved
@tomschr tomschr force-pushed the feature/303-refactor-__init__ branch from 14e4f96 to 93eebfe Compare November 20, 2020 20:49
@tomschr
Copy link
Member Author

tomschr commented Nov 20, 2020

@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 _enforce_str() function. This makes the __init__ function a bit more readable. Maybe the function could be better named (as they return not only a string, but also None, so... 🤔 ).

Apart from this, I'm not sure about the @overload decorator. Probably I'm doing something wrong or this complexity cannot be solved through this.

Regarding the __init__ function...
On the one side, it greatly enhances the usability. Developers don't have to use cumbersome methods now, they just throw into the class whatever is appropriate. I like that.
On the other side, the initializer has ~63 lines. That's a lot for such a function. Not sure if we can simplify that without compromising the usability.

Thoughts? 🙂

@tomschr tomschr force-pushed the feature/303-refactor-__init__ branch from f9ef14f to edf398b Compare November 21, 2020 11:55
@tomschr tomschr added this to In progress in Release 3.1.0 via automation Jan 31, 2022
@tomschr tomschr force-pushed the feature/303-refactor-__init__ branch 2 times, most recently from 66af43c to ff470fa Compare January 31, 2022 08:41
tomschr and others added 4 commits March 7, 2023 08:14
* 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
@tomschr tomschr force-pushed the feature/303-refactor-__init__ branch from 282e9c7 to 26ab6d0 Compare March 7, 2023 07:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancement Not a bug, but increases or improves in value, quality, desirability, or attractiveness Release_3.x.y Only for the major release 3
Projects
No open projects
Release 3.1.0
In progress
Development

Successfully merging this pull request may close these issues.

Rethinking __init__ of VersionInfo class?
2 participants