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

Rethinking __init__ of VersionInfo class? #303

Open
tomschr opened this issue Oct 26, 2020 · 2 comments · May be fixed by #317
Open

Rethinking __init__ of VersionInfo class? #303

tomschr opened this issue Oct 26, 2020 · 2 comments · May be fixed by #317
Labels
Design Ideas, suggestions, musings about design questions Enhancement Not a bug, but increases or improves in value, quality, desirability, or attractiveness Question Unclear or open issue subject for debate Release_3.x.y Only for the major release 3
Projects

Comments

@tomschr
Copy link
Member

tomschr commented Oct 26, 2020

Situation

Originally posted by @tomschr in #258 (comment)

From the above discussion, I thought it would be worth to decouple the compare discussion from the initializer discussion. Both are somewhat related, but can live independently.

With a more "advanced" initializer/constructor we get the following benefits:

  • more "pythonic": it's one, obvious way to get an instance.
  • avoids the longer function call Version.parse(...).
  • more readable

With such an (overloaded?) initializer, we could cover the following use cases:

>>> from semver import Version
>>> Version(1)
Version(major=1, minor=0, patch=0, prerelease=None, build=None)
>>> Version(1, "4", b"5")
Version(major=1, minor=4, patch=5, prerelease=None, build=None)
>>> Version(1, patch=2)
Version(major=1, minor=0, patch=2, prerelease=None, build=None)
>>> Version("1.2.3")
Version(major=1, minor=2, patch=3, prerelease=None, build=None)
>>> Version(b"1.2.3")
Version(major=1, minor=2, patch=3, prerelease=None, build=None)
>>> v = Version(b"2.3.4")
>>> Version(v)
Version(major=2, minor=3, patch=4, prerelease=None, build=None)
>>> t = (1, 2, 3)
>>> Version(*t)                                             
Version(major=1, minor=2, patch=3, prerelease=None, build=None)
>>> d = {'major': 3, 'minor': 4, 'patch': 5,  'prerelease': 'pre.2', 'build': 'build.4'}
>>> Version(**d)
Version(major=3, minor=4, patch=5, prerelease='pre.2', build='build.4')

Discussions and Possible Solutions

To implement a somewhat more advanced constructor/initializer, we have these options:

  1. Program it manually with isinstance and if...else constructs
  2. Use the typing.overload function (suggested by @tlaferriere)
  3. Use functools.singledispatch

However, all three comes at a cost or an issue:

  1. Maybe not impossible, but the code would look probably ugly.
  2. "The @overload decorator is purely for type hints, you can only specify one function body and it has to distinguish the different types using isinstance.` (Originally posted by @tlaferriere in Consider keeping compare module level function #258 (comment))
  3. Is not possible with an __init__ method. The singledispatch works only for functions(!), not methods. For methods we would need functools.singledispatchmethod which is only available from Python >= 3.8.

Another idea goes into a completely different direction. Maybe we shouldn't change the Version class much, but offer a much shorter variant: semver.v.

from functools import singledispatch

# ...
@singledispatch
def ver(major, minor=0, patch=0, prerelease=None, build=None) -> "Version":
    return Version(major, minor, patch, prerelease, build)
    
@ver.register(bytes)
@ver.register(str)
def _(ver: str) -> "Version":
    if isinstance(ver, bytes):
       ver = str(ver, "utf-8")
    if "." in ver:
        return Version.parse(ver)
    return Version(int(ver))

@ver.register(Version)
def _(ver: "Version") -> "Version":
    return ver

Which means, we could just use semver.v as a much shorter variant:

>>> import semver
>>> semver.v("1.2.3")
Version(major=1, minor=2, patch=3, prerelease=None, build=None)

One drawback could be that v is quite short. Maybe too short? Especially if you import it with from semver import v it could be easily overwritten by other, local variables.
That could be a bit avoided to use capital V or ver. Or we use the much longer name semver.version.

Thoughts? Any other ideas? Would that be worth the effort?

@tlaferriere @gsakkis @ppkt

@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 Oct 26, 2020
@tomschr tomschr added this to To do in Release 3 via automation Oct 26, 2020
@tomschr tomschr added the Question Unclear or open issue subject for debate label Oct 26, 2020
@tomschr
Copy link
Member Author

tomschr commented Nov 5, 2020

I found another solution which doesn't need overloading. The previous signature of __init__ was this:

    def __init__(
        major: SupportsInt,
        minor: SupportsInt = 0,
        patch: SupportsInt = 0,
        prerelease: Union[String, int] = None,
        build: Union[String, int] = None,
        )

This changed now to this:

    def __init__(self, *args: Tuple, **kwargs: Dict):
        # print(f">> args={args} {len(args)}, kwargs={kwargs} {len(kwargs)}")
        verlist = [None, None, None, None, None]

        if not args and not kwargs:
            raise ValueError("No argument given.")

        if len(args) > 5:
            raise ValueError("You cannot pass more than 5 arguments to Version")

        if args and "." in str(args[0]):
            # we have a version string as first argument
            cls = self.__class__
            v = cls._parse(args[0])
            self._major = int(v["major"])
            self._minor = int(v["minor"])
            self._patch = int(v["patch"])
            self._prerelease = v["prerelease"]
            self._build = v["build"]
            return

        for index, item in enumerate(args):
            verlist[index] = args[index]

        # Build a dictionary of the arguments except prerelease and build
        try:
            version_parts = {
                # Prefer kwargs over args
                "major": int(kwargs.pop("major", 0) or verlist[0] or 0),
                "minor": int(kwargs.pop("minor", 0) or verlist[1] or 0),
                "patch": int(kwargs.pop("patch", 0) or verlist[2] or 0)
            }
        except ValueError:
            raise ValueError("Expected integer or integer string for major, "
                             "minor, or patch")

        prerelease = kwargs.pop("prerelease", None)
        build = kwargs.pop("build", None)
        # Check, if kwargs contains other unknown keys:
        if kwargs:
            raise ValueError("Unknown key passed in {n}: {k}".format(
                n=self.__class__.__name__,
                k=", ".join([f"{k}={v}"
                             for k, v in kwargs.items()])))

        for name, value in version_parts.items():
            if value < 0:
                raise ValueError(
                    "{!r} is negative. A version can only be positive.".format(name)
                )

        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)

This allows some interesting variations:

>>> import semver
>>> semver.Version(1, 2, 3)
Version(major=1, minor=2, patch=3, prerelease=None, build=None)
>>> semver.Version("1.2.3")
Version(major=1, minor=2, patch=3, prerelease=None, build=None)
>>> semver.Version(1, minor="2", patch=3)
Version(major=1, minor=2, patch=3, prerelease=None, build=None)

You can also use positional arguments and keyword arguments to "overwrite" one or the other:

>>> semver.Version(1, major="2", patch=3)
Version(major=2, minor=0, patch=3, prerelease=None, build=None)

Probably the code can be improved, but it's just a proof-of-concept to show this idea. Additionally, it avoids the complicated overload code. What do you think?

tomschr added a commit to tomschr/python-semver that referenced this issue Nov 6, 2020
* Allow different variants to call Version
* Adapt the documentation and README
* Adapt and amend tests
* Add changelog entries

TODO: Fix typing errors
@tomschr
Copy link
Member Author

tomschr commented Nov 6, 2020

If the above PR got accepted, the Version.parse would become obsolete. In that case, I would suggest to deprecate it.

@tomschr tomschr moved this from To do to In progress in Release 3 Nov 6, 2020
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 8, 2020
* Allow different variants to call Version
* Adapt the documentation and README
* Adapt and amend tests
* Add changelog entries

TODO: Fix typing errors
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 8, 2020
* Allow different variants to call Version
* Adapt the documentation and README
* Adapt and amend tests
* Add changelog entries

TODO: Fix typing errors
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 8, 2020
* Allow different variants to call Version
* Adapt the documentation and README
* Adapt and amend tests
* Add changelog entries
* Introduce a (private) _ensure_str class method
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 8, 2020
* Allow different variants to call Version
* Adapt the documentation and README
* Adapt and amend tests
* Add changelog entries
* Introduce a (private) _ensure_str class method
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 8, 2020
* Allow different variants to call Version
* Adapt the documentation and README
* Adapt and amend tests
* Add changelog entries
* Introduce a (private) _ensure_str class method
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 8, 2020
* Allow different variants to call Version
* Adapt the documentation and README
* Adapt and amend tests
* Add changelog entries
* Introduce a (private) _ensure_str class method
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 8, 2020
* Allow different variants to call Version
* Adapt the documentation and README
* Adapt and amend tests
* Add changelog entries
* Introduce a (private) _ensure_str class method
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 8, 2020
* Allow different variants to call Version
* Adapt the documentation and README
* Adapt and amend tests
* Add changelog entries
* Introduce a (private) _ensure_str class method
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 8, 2020
* 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
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 10, 2020
* 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
@tomschr tomschr linked a pull request Nov 10, 2020 that will close this issue
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 10, 2020
* 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
@tomschr tomschr mentioned this issue Nov 11, 2020
4 tasks
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 12, 2020
* 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
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 21, 2020
* 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
tomschr added a commit to tomschr/python-semver that referenced this issue Jan 31, 2022
* 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
@tomschr tomschr added the Design Ideas, suggestions, musings about design questions label Nov 23, 2022
tomschr added a commit to tomschr/python-semver that referenced this issue Mar 7, 2023
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Ideas, suggestions, musings about design questions Enhancement Not a bug, but increases or improves in value, quality, desirability, or attractiveness Question Unclear or open issue subject for debate Release_3.x.y Only for the major release 3
Projects
No open projects
Release 3
  
In progress
Development

Successfully merging a pull request may close this issue.

1 participant