Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Add __getitem__ method to allow easy access to version parts #138

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

Closed
wants to merge 13 commits into from
Closed

Add __getitem__ method to allow easy access to version parts #138

wants to merge 13 commits into from

Conversation

tlaferriere
Copy link
Contributor

@tlaferriere tlaferriere commented Aug 16, 2019

This is a natural extension of the work done in #124 (and parents) and makes it easy to access version parts using their index.

Use case:

Using an Enum to access the version part:

VersionPartEnum = Enum('VersionPartEnum', "MAJOR MINOR PATCH PRERELEASE BUILD")
version_info = VersionInfo.parse("1.2.3-rc.1+build.13")

print(version_info[VersionPartEnum.MINOR - 1])  # 2

@s-celles
Copy link
Member

Thanks @tlaferriere for this PR
but a feature should never be implemented without unit tests.

@tlaferriere
Copy link
Contributor Author

I'll get on it.

@tlaferriere
Copy link
Contributor Author

@scls19fr Should I add test vectors for edge cases or are these ones satisfactory?

@s-celles
Copy link
Member

I would be pleased to have the general consensus about this feature before merging
Pinging @k-bx @kxepal @bittner @tomschr @ppkt

@bittner
Copy link
Contributor

bittner commented Aug 20, 2019

The implementation is certainly fine.

I'm not super-convinced by the above use case, though. While it's a fact that Enums start at 1 for a reason I'd prefer to see a more intuitive syntax (i.e. something that doesn't need to do index calculations, namely - 1). Then, if this is a real use case, why do we require the user to come up with the needed Enum?

The doctests of parse_version_info may also include access with constants, and we may provide the constants directly, e.g.

>>> import semver
>>> version_info = semver.parse_version_info("3.4.5-pre.2+build.4")
>>> version_info.major == version_info[semver.MAJOR] == 3
True
>>> version_info.minor == version_info[semver.MINOR] == 4
True
...

MAJOR & friends would have to be (read-only) class members for this to work, obviously.

Copy link
Member

@ppkt ppkt left a comment

Choose a reason for hiding this comment

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

@tlaferriere - overall I'm happy with these changes - we are already supporting iteration over VersionInfo instance so, in my opinion, it's a quite nice feature to have an ability to have a support for indexing.

I have only some small remarks regarding your PR.

semver.py Outdated Show resolved Hide resolved
test_semver.py Show resolved Hide resolved
@tomschr
Copy link
Member

tomschr commented Aug 22, 2019

@bittner I like your idea! 👍 😃

@s-celles
Copy link
Member

s-celles commented Oct 1, 2019

This PR have a conflicting file (sorry I should have managed this one before but was and still is quite busy).
Please resolve conflicts and thumb up if you are ok to merge this.
Thumb down if you don't want this PR to be merged.

@s-celles
Copy link
Member

s-celles commented Oct 8, 2019

Please fix this conflict to have a chance to have this feature include in latest release!

@tlaferriere
Copy link
Contributor Author

Excuse the delay, I am resolving the conflicts and finishing up tests.

…ility function for check the validity of the index.
@tlaferriere
Copy link
Contributor Author

tlaferriere commented Oct 8, 2019

So I've decided to disallow negative indexing as it would require going against the nature of a tuple to make behave like List in essence. I've tried to cover all edge cases that could come up with a slice in the _validate_index protected method. If you spot edge cases that make it fail let me know 😉.
Edit: just found some edge cases.
Edit again: Changing my mind about negative indexing. Trying to block negative indexing while allowing slices is not easy at all. Too many edge cases that have to be handled manually, which is not very pythonic IMO.

@tlaferriere
Copy link
Contributor Author

Also I've decided not to add the MAJOR, MINOR, etc. members as it feels redundant with the key indices like "major", "minor", etc. After all, the goal was to access it like an iterable, and not a dictionary.

@s-celles
Copy link
Member

s-celles commented Oct 8, 2019

Thanks @tlaferriere for this first contribution here.

Could you update doc to show this new feature?
I wonder if we shouldn't have example in docstring also.

Could you also update CHANGELOG?

@s-celles
Copy link
Member

s-celles commented Oct 8, 2019

You can also add your name to https://github.com/k-bx/python-semver/blob/master/CONTRIBUTORS

semver.py Outdated
@@ -168,6 +168,12 @@ def __iter__(self):
for v in self._astuple():
yield v

def __getitem__(self, index):
"""Implement getitem. This automatically strips empty parts of the
version from the iterable from which the index is taken."""
Copy link
Member

@tomschr tomschr Oct 8, 2019

Choose a reason for hiding this comment

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

Thanks @tlaferriere, could you change the docstring:

Suggested change
version from the iterable from which the index is taken."""
version from the iterable from which the index is taken.
:param int index: a positive or negative integer indicating
the offset
:raises: IndexError, if index is beyond the range
"""

Copy link
Member

Choose a reason for hiding this comment

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

@tomschr according to current implementation, index can be negative.

@tlaferriere I think there is a subtle bug in your implementation:

    def test_example():
        v = VersionInfo.parse('1.2.3+build0')
>       assert v[:] == (1, 2, 3, None, 'build0')
E       AssertionError: assert (1, 2, 3, 'build0') == (1, 2, 3, None, 'build0')
E         At index 3 diff: 'build0' != None
E         Right contains more items, first extra item: 'build0'
E         Use -v to get the full diff

i.e. if "prerelease" part is missing and "metadata" is present, after using getitem , "metadata" appears at index for "prerelease"

Copy link
Member

Choose a reason for hiding this comment

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

@ppkt

according to current implementation, index can be negative.

You are right, I've corrected the GH suggestion. Thanks! 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After much deliberation with myself and an increasingly complex function, I have decided to rewrite it (multiple times). I believe I have made this implementation as simple as possible. Turns out the subtle bug mentioned by @ppkt was a symptom of a few bad design decisions on my part. Now it doesn't support negative indices.

Co-Authored-By: Tom Schraitle <tomschr@users.noreply.github.com>
@s-celles
Copy link
Member

Conflicting files need to be solved here also. CI is failing and should be fixed also.

@s-celles s-celles requested a review from tomschr November 20, 2019 05:29
:param Union[int, slice] index: a positive integer indicating the
offset or a slice
:raises: IndexError, if index is beyond the range or a part is None
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

This docstring has an unusual indentation. According to PEP 257 indentation may look like:

def foo():
    """First line of comment

    Second line of comment. The longer description.
    """

(what I like less but seems to be favored by Guido & friends), or:

def foo():
    """
    First line of comment

    Second line of comment. The longer description.
    """

Same below, in the method following next.

Copy link
Member

Choose a reason for hiding this comment

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

Good point, but then the other docstrings needs to be adapted as well. Maybe we could do that in a combined issue later?

else:
slice_is_full = True
part = self._astuple()[index]
for i in part:
Copy link
Contributor

Choose a reason for hiding this comment

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

What is i here? Pylint would be complaining (if you ran it). Can you use a self-explanatory name?

Copy link
Member

Choose a reason for hiding this comment

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

One idea could be to replace i with item.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess I could use subpart instead.

Copy link
Member

Choose a reason for hiding this comment

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

See also the more recent comments below. 😉

raise negative_error
else:
part = self._astuple()[index]
if part is None:
Copy link
Contributor

Choose a reason for hiding this comment

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

if not part:

... is probably more pythonic. Or is there a reason for an explicit None?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is in several places in the code, by the way, and could be made pythonic throughout the code.

Copy link
Member

@tomschr tomschr left a comment

Choose a reason for hiding this comment

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

Just a small comment. 😉

:param Union[int, slice] index: a positive integer indicating the
offset or a slice
:raises: IndexError, if index is beyond the range or a part is None
"""
Copy link
Member

Choose a reason for hiding this comment

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

Good point, but then the other docstrings needs to be adapted as well. Maybe we could do that in a combined issue later?

@tomschr
Copy link
Member

tomschr commented Nov 20, 2019

@tlaferriere Found another issue.

You could simplify the code a bit and omit the else branches. Could help to improve readability:

def __getitem__(self, index):
        """
           Implement getitem. If the part requested is undefined, or a part of the
           range requested is undefined, it will throw an index error.
           Negative indices are not supported

           :param Union[int, slice] index: a positive integer indicating the
                  offset or a slice
           :raises: IndexError, if index is beyond the range or a part is None
           """
        undefined_error = IndexError("Version part undefined")
        negative_error = IndexError("Version index cannot be negative")
        if isinstance(index, slice):
            if (False if index.start is None else index.start < 0) or \
                    (False if index.stop is None else index.stop < 0):
                raise negative_error
            slice_is_full = True
            part = self._astuple()[index]
            for item in part:
                if item is None:
                    slice_is_full = False
                elif not slice_is_full:
                    raise undefined_error
            part = tuple(filter(None, part))
        else:
            if index < 0:
                raise negative_error
            part = self._astuple()[index]
            if part is None:
                raise undefined_error
        return part

@s-celles s-celles requested a review from tomschr November 20, 2019 20:34
Copy link
Member

@tomschr tomschr left a comment

Choose a reason for hiding this comment

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

@tomschr
Copy link
Member

tomschr commented Nov 24, 2019

@tlaferriere
I've played around with the code a bit and find a quite readable version. It reduces the line count from 21 to 12. 👀 It also avoids the for-loop. Maybe you can try it out?

def __getitem__(self, index):
        """Implement getitem. If the part requested is undefined, or a part of the
           range requested is undefined, it will throw an index error.
           Negative indices are not supported

           :param Union[int, slice] index: a positive integer indicating the
                  offset or a slice
           :raises: IndexError, if index is beyond the range or a part is None
           :return: the requested part of the version at position index
        """
        if isinstance(index, int):
            index = slice(index, index+1)

        if isinstance(index, slice) and \
           (index.start is None or index.start < 0) and \
           (index.stop is None or index.stop < 0):
            raise IndexError("Version index cannot be negative")

        # Could raise IndexError:
        part = tuple(filter(None, self._astuple()[index]))

        if len(part) == 1:
            part = part[0]
        if not part:
            raise IndexError("Version part undefined")
        return part

Tests were successful. 🎉

@tomschr
Copy link
Member

tomschr commented Dec 10, 2019

@tlaferriere Thomas, what's your opinion on #138 (comment)?

Could we finish this PR to publish it for the 2.9.1 release or do you need more time? 😉 If you don't have time yet due to the holiday season, I could finish it for you if you like. Let me know what you prefer. 😉

@tlaferriere
Copy link
Contributor Author

@tomschr with my finals ending soon, I figured I would have more time to finish it up and get it ready for the release. I like the snippet, I haven't had time to inspect it, but it sure seems cleaner. I'll get some work in the next few days and hopefully it will go well enough for it to be ready.

@tomschr
Copy link
Member

tomschr commented Dec 11, 2019

Take your time. If you need any help, let us know. 😊

@tomschr
Copy link
Member

tomschr commented Feb 20, 2020

@tlaferriere Thomas, just a quick question: if you are too busy at the moment (which is completely fine!), should I finish the issue for you or would you prefer to do it yourself? Of course I will add you as co-author. 😉

tomschr added a commit to tomschr/python-semver that referenced this pull request Apr 26, 2020
* Add __getitem__ to VersionInfo class
* Add test cases
* Add user documentation
* Extend CHANGELOG


Co-authored-by: Thomas Laferriere <tlaferriere@users.noreply.github.com>
Co-authored-by: Peter Bittner <bittner@users.noreply.github.com>
Co-authored-by: Karol Werner <ppkt@users.noreply.github.com>
Co-authored-by: Sébastien Celles <scls19fr@users.noreply.github.com>
@tomschr
Copy link
Member

tomschr commented Apr 26, 2020

I've created this PR anew in #243, so closing this.

@tomschr tomschr closed this Apr 26, 2020
tomschr added a commit that referenced this pull request May 1, 2020
@tomschr tomschr mentioned this pull request May 1, 2020
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.