Consider keeping `compare` module level function
See original GitHub issueI 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:
- Created 3 years ago
- Comments:13 (11 by maintainers)
Top GitHub Comments
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 soVersionInfo("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
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 forVersionInfo.parse()
and that it would allow you to use all the features of theVersionInfo
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:
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.
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: thecmp
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:
And now we have two ways to compare version strings: the
semver.compare
function and theVersionInfo.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:
And I tend to think that this is a special case.
… … … … …
@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 toVersionInfo.__init__
so that we can construct aVersionInfo
object from a string, which I believe would be highly practical.Finally, I’ve never been a fan of the name
VersionInfo
. I think theInfo
part is redundant with the fact that it’s a class, kind of like naming a variable like so:So there are my two suggestions for this issue:
__init__(self, version: str)
VersionInfo
toVersion
With these, you could just write
Version("1.2.3").compare("1.2.4")
and this should increase the social acceptability of removingsemver.compare
, while adhering to The Zen of PythonOne obvious way to do it
. … …