question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Consider keeping `compare` module level function

See original GitHub issue

I would think that comparing two semver strings is a pretty common use case and this code:

semver.compare("1.2.3", "3.2.1")

is arguably a lot nicer than

semver.VersionInfo.parse("1.2.3").compare("3.2.1")

plus it is probably already present in a large number of projects that use this library, so removing it entirely should be done only if absolutely necessary (which I do not believe that it is).

The fact that VersionInfo.compare accepts the other argument as a string says a lot about the convenience and desirability of working with strings. But it leaves us in this weird situation where VersionInfo.compare takes 2 arguments (self, and other) where self must be a VersionInfo object but other can be one of many types. If we were to write out the type signature we would have something like:

VersionInfo.compare(self: VersionInfo, other: Union[str, dict, list, tuple, VersionInfo])

(Another quirk of this function is that given a tuple as an argument, it will convert to VersionInfo and then immediately back to a tuple again.)

I think as far as API design goes, it doesn’t make a lot of sense for the main comparison function to have this type signature. Why is only one argument required to be a parsed VersionInfo object? Why are we doing automatic conversion on one argument but not the other? What if I already have both arguments in tuple form and don’t want to perform two useless conversions?

I would suggest having the module level API be a sort of convenience wrapper around the VersionInfo API. I think it is likely that the vast majority of semver data starts out in string form, so there should be a quick and clean API that one can call on this data. I would suggest making semver.compare look like this:

semver.compare(left: Union[str, dict, list, tuple, VersionInfo], right: Union[str, dict, list, tuple, VersionInfo])

And I would probably avoid double conversions on lists and tuples.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Comments:13 (11 by maintainers)

github_iconTop GitHub Comments

2reactions
tlaferrierecommented, Jun 18, 2020

I must agree that dealing with version strings is a major use case (not to say the main one) and being able to operate on them directly is extremely convenient. That said, it is hard to standardize a module level API that can deal with alternative representations of the same idea, and when you lack standardization, maintainability takes a hit. In this case, I think maintainability is more important than convenience and wins out.

This does raise the issue that typing out VersionInfo.parse (that is if you like importing names from other namespaces directly into yours) does eat up almost 20% of your horizontal real estate (of the unofficial 88 character limit that is quite widespread in python). I have found this to be rather inconvenient at times. This could be easily (from the user pov) avoided by implementing a constructor that can accept a single string like so VersionInfo("1.2.3"). The issue is that it would increase the complexity of the constructor for yet another shortcut.

This gives me an idea: there exist many types of string literals in python (f-strings, r-strings) that use a prefixed letter to distinguish themselves. How hard would it be to add one more (a v-string) that you can activate through an import? (I’m not very familiar with the internals of python, but I suspect this could involve changing the python syntax). But let’s forget about the potential hardships of implementing such a feature for a moment. How awesome would it be if you could write

v"1.2.3".compare(version_string)

And I believe that writing version strings like that is so natural even non-technical people could understand the meaning of this line given the fact that v is used in so many places to indicate a version number. I envision this feature would basically be syntactic sugar for VersionInfo.parse() and that it would allow you to use all the features of the VersionInfo class from a literal.

In the end, I think that the concision of some obvious and common usages should be addressed and could be improved. I think we could add an example of an alias in the docs like so:

import semver

v = semver.VersionInfo.parse

v("1.2.3").compare("3.2.1")
1reaction
tlaferrierecommented, Jun 24, 2020

Sorry to play the devil’s advocate here, but I think it is a good way to really make sure every possible aspect of a decision is thoroughly covered.

Python is a language of convenience

Indeed, in the case of Python prior to 3. From Python 3 onward (I’d say post PEP 484), it is now a major language that is used in production that still values convenience, but only where it doesn’t hurt maintainability (type hints aren’t exactly convenient to type out, but are extremely valuable for maintainability). Also, why would you replace the print statement with an actual function call? Well it was in the goal of increasing the coherence of the language (so that it makes more sense). Last, but not least of python 3 changes: the cmp function is gone, and I think that goes a long way telling us there are better ways comparing things than checking a numeric return value of a function.

Although Python is a convenient language, I think it is attributable to the fact that it is excellently designed around a certain philosophy: The Zen of Python. In this philosophy, I think the most known tenet is:

There should be one-- and preferably only one --obvious way to do it.

And now we have two ways to compare version strings: the semver.compare function and the VersionInfo.compare method. I’m convinced that this is a clear violation of the previous edict.

Now I understand this is a special case where the current alternative is a character count monstrosity, but as The Zen of Python says:

Special cases aren't special enough to break the rules.

And I tend to think that this is a special case.

… … … … …

Although practicality beats purity.

@tomschr I’ve been reading up on mypy recently and I’ve come across an interesting feature: the @overload decorator. It allows you create multiple precise (and unforgiving) type signatures for a method or function. I think anyone who has once programmed C++ can attest the power of function overloading in a typed language. Well I think this could be applicable to VersionInfo.__init__ so that we can construct a VersionInfo object from a string, which I believe would be highly practical.

Finally, I’ve never been a fan of the name VersionInfo. I think the Info part is redundant with the fact that it’s a class, kind of like naming a variable like so:

name_string: str = "Thomas"

So there are my two suggestions for this issue:

  • Overload the constructor with __init__(self, version: str)
  • Rename VersionInfo to Version

With these, you could just write Version("1.2.3").compare("1.2.4") and this should increase the social acceptability of removing semver.compare, while adhering to The Zen of Python One obvious way to do it. … …

Although that way may not be obvious at first unless you're Dutch.
Read more comments on GitHub >

github_iconTop Results From Across the Web

python - @staticmethod vs module-level function
I've googled this question, and it seems there's some general agreement that module-level functions are preferred over static methods because it's more pythonic ......
Read more >
python - Module function vs staticmethod vs classmethod vs ...
Keeps functions related to the class inside the class and out of the module namespace. Allows calling the function on instances of the...
Read more >
inspect — Inspect live objects — Python 3.11.1 documentation
The inspect module provides several useful functions to help get information about live objects such as modules, classes, methods, functions, tracebacks, frame ...
Read more >
Modules · The Julia Language
We discuss the related concepts and functionality below in detail. ... Usually, import ModuleName is used in contexts when the user wants to...
Read more >
4. Code Reuse: Functions and Modules - Head First Python ...
Python uses the name “function” to describe a reusable chunk of code. Other programming languages use names such as “procedure,” “subroutine,” and “method.” ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found