Skip to content

Feature request: concise "compatibility" matching #284

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
sbrudenell opened this issue Oct 15, 2020 · 23 comments
Closed

Feature request: concise "compatibility" matching #284

sbrudenell opened this issue Oct 15, 2020 · 23 comments
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

Comments

@sbrudenell
Copy link
Contributor

I'd really like a concise expression for "is version x compatible with version y?"

Specifically, suppose I write code against one version of an API, and the API exposes a semver-compliant version. Is v_caller compatible with v_api?

The most concise expression I can come up with is something like v_caller <= v_api and v_caller.major == v_api.major, but I really wish this was simpler and more readable. Maybe v_caller.compatible(v_api); or maybe v_caller.can_call(v_api) better illustrates which operand is which.

I know this would be covered by #241, but its scope is quite broad. I wanted to specifically call out this feature.

@tomschr tomschr added Enhancement Not a bug, but increases or improves in value, quality, desirability, or attractiveness Release_3.x.y Only for the major release 3 labels Oct 21, 2020
@tomschr
Copy link
Member

tomschr commented Oct 21, 2020

@sbrudenell Thanks for your report.

I'd really like a concise expression for "is version x compatible with version y?"

Yes, but what you mean by "compatible with"? I have a vague idea, but "In the face of ambiguity, refuse the temptation to guess." 😉

Could you help me to understand what you mean by giving a more verbose example? Thanks! 👍

@sbrudenell
Copy link
Contributor Author

Maybe a clearer way to say what I mean is: x -> y is not a breaking change.

Another way: x matches "^y" according to npm version matching idioms. (or probably a subset of the idiom, not treating version 0 as special)

Does that make sense?

@Lexicality
Copy link
Contributor

Yes, but what you mean by "compatible with"? I have a vague idea, but "In the face of ambiguity, refuse the temptation to guess." 😉

For my use case I'd most like to be able to use the PEP 440 version of "compatible": https://www.python.org/dev/peps/pep-0440/#compatible-release

I would also greatly appreciate being able to use the operators from that pep in the code, for example

>>> semver.match("1.2.3", "==1.*")
True

@rafalkrupinski
Copy link
Contributor

Isn't the specification enough?
SemVer is all about compatibility...

@Lexicality
Copy link
Contributor

Isn't the specification enough?
SemVer is all about compatibility...

The spec is about API compatibility, but doesn't have any info about version compatibility, eg ~=1.2

@rafalkrupinski
Copy link
Contributor

The spec is about API compatibility, but doesn't have any info about version compatibility, eg ~=1.2

Did you mean version matching?

OP asked for a method to check if a caller implementing version X is compatible with a callee implementing version Y, using somthing like X.compatible_with(Y).

I'd really like a concise expression for "is version x compatible with version y?"

That's exactly what SemVer is about.
In order to do that, compatible_with function should simply apply the rules described in the spec, in reverse.

Yes, but what you mean by "compatible with"?

This is where I'm lost, SemVer describes exactly the rules to answer that.

tomschr added a commit to tomschr/python-semver that referenced this issue Nov 15, 2022
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 15, 2022
* Introduce Spec class to deal with such comparisons
* Improve documentation
* Simplify code in Version.match (delegates to Spec.match)
@tomschr
Copy link
Member

tomschr commented Nov 15, 2022

Funny coincidence, I've played around with these extended matching parts in PR #367. 😄 It may or may not be integrated into the main branch, depending on the outcome of your comments. If you want to give it a try, please comment on the respective PR.

It goes in the direction to #241.

@sbrudenell, @Lexicality and @rafalkrupinski would that help? Any comments are greatly appreciated.

@rafalkrupinski
Copy link
Contributor

I'm looking for trivial matching according to semver rules

  • if major is different, it's incompatible
  • if major is equal but minor is lower, it's incompatible
  • if Prerelease is present and different, it's incompatible
  • otherwise it's compatible

@tomschr
Copy link
Member

tomschr commented Nov 15, 2022

Would something like this be useful?

def iscompatible(v1: Version, v2: Version):
     if v1.major != v2.major:
         return False
     if v1.minor < v2.minor:
         return False
     if v1.prerelease != v2.prerelease:
         return False
     return True

>>> v1 = Version(1, 2, 3)
>>> v2 = Version(1, 1, 4)
>>> v3 = Version(1, 3, 5)

>>> iscompatible(v1, v2)
True
>>> iscompatible(v1, v3)
False

The code is derived from your description. Certainly we could optimize it.

@rafalkrupinski
Copy link
Contributor

@tomschr
Yes, that's roughly what I was looking for.
It should follow the specification rather than my brief summary, but I think the only thing I've missed is that versions starting with 0 are incompatible with each other unless identical.

Also, I'd rather have a method instead of a function, so there is a clear this and that, since compatibility is directed, i.e.

  • caller 1.1 is compatible with callee 1.2, but
  • caller 1.2 is not compatible with callee 1.1.

I'd like to put a version in my model classes and yaml files, so the program can verify if it can handle the files, similar to Kubernetes.

Cheers

@tlaferriere
Copy link
Contributor

tlaferriere commented Nov 15, 2022

Handling the semantics of the distinction between caller and callee is going to be a bit of a job. The most obvious but implicit way I can think of is encoding this information in the order of the arguments to the function, like so:

def iscompatible(caller: Version, callee: Version):
    ...

In a more explicit way, but still mostly implicit, there is the option of creating a method on Version where self is considered the caller and the other version the callee:

def iscompatible(self, callee: Version):
    ...

As I write it down, I realise that having these arg names actually makes them very explicit, so they may not be that bad choices in the end.

@rafalkrupinski
Copy link
Contributor

rafalkrupinski commented Nov 15, 2022

Not a native English speaker, but how about naming it compatible_with? Wouldn't it make the direction clear?

is X compatible with Y?

if x.compatible_with(y):

@tlaferriere
Copy link
Contributor

The actual factor that is directional is the dependency relation. The word with is not directional (meaning there is no difference between x is compatible with y and y is compatible with x if you know nothing else about x and y). Using the phrasing compatible_with does sound nice and natural though, so it still could be an excellent candidate for the method (or function) name. But the args would still have to contain the semantics of the dependency direction to make sense.

def compatible_with(self, callee: Version) -> bool:  # Quite natural
    ...

There is the convention of prefixing names for boolean values with is, maybe this would be nice (if it isn't too verbose):

def is_compatible_with(self, callee: Version) -> bool:
    ...

@sbrudenell
Copy link
Contributor Author

For naming, I've been using variations on breaking, like

def is_breaking(v_from, v_to):
   ...

It implies more direction than "compatible with"

@tomschr
Copy link
Member

tomschr commented Nov 16, 2022

Both is_compatible and is_breaking are possible. I have three questions:

  1. Should we make is_compatible an alias of is_breaking? Or would it be confusing? Probably there should be only one way to do it (according to the Zen of Python).
  2. We have a class method isvalid without an underscore. As the naming should be consistent, should we:
    1. Rename isvalid -> is_valid?
      As we are approaching a major release, it would be possible to introduce a breaking change. We could mitigate this break by creating an alias and deprecate the isvalid method.
    2. Rename is_compatible -> iscompatible (likewise is_breaking)?
  3. Should the method is_compatible only accept Version? Or should it allow other types like strings, tuples, dicts etc.?

Thanks!

@rafalkrupinski
Copy link
Contributor

Both is_compatible and is_breaking are possible. I have three questions:

1. Should we make `is_compatible` an alias of `is_breaking`? Or would it be confusing?

An alias would be very confusing, since is 'breaking' is opposite of 'compatible'. ;)

2. We have a class method `isvalid` _without_ an underscore. As the naming should be consistent, should we:
   
   1. Rename `isvalid` -> `is_valid`?
      As we are approaching a major release, it would be possible to introduce a breaking change. We could mitigate this break by creating an alias and deprecate the `isvalid` method.
   2. Rename `is_compatible` -> `iscompatible` (likewise `is_breaking`)?

I'm not a user yet, so I don't have a stake here.
According to SemVer, there is nothing wrong with breaking changes in pre-release.

3. Should the method `is_compatible` only accept Version? Or should it allow other types like strings, tuples, dicts etc.?

Good question.
Functions should do one thing, but it would be a convenient shortcut if it did allow other types. Though that would mean it does two things - conversion/validation and comparison.
What do other functions accept?

tomschr added a commit to tomschr/python-semver that referenced this issue Nov 16, 2022
The algorithm checks:

* if the two majors are different, it's incompatible
* if the two majors are equal, but the minor of the callee
  is lower than the caller, it's incompatible
* if both prereleases are present and different,
  it's incompatible
* otherwise it's compatible
@Lexicality
Copy link
Contributor

Lexicality commented Nov 16, 2022

From what I understand, the current a.is_compatible(b) shows if you could replace package b with package a without any problems right?
I don't think the is_breaking would reflect that behaviour at all?

Just so I understand

  • 1.2.0 is compatible with 1.1.1
  • 2.0.0 is not compatible with 1.1.1
  • 1.1.1 is compatible with 1.1.1
  • 1.1.0 is not compatible with 1.1.1

Right?

@tomschr
Copy link
Member

tomschr commented Nov 16, 2022

  1. [...]
    An alias would be very confusing, since is 'breaking' is opposite of 'compatible'. ;)

Possibly yes. 🙂 My brain didn't catch that in the early morning. 😄

  1. [...]
    According to SemVer, there is nothing wrong with breaking changes in pre-release.

Yeah, I'm inclined to make this breaking change in the upcoming semver 3 version.

  1. [...]
    Functions should do one thing, but it would be a convenient shortcut if it did allow other types. Though that would mean it does two things - conversion/validation and comparison.

That's a good point. It seems, that in most cases, people use string types a lot. Maybe let's wait what others have to say here. Maybe they come up with some good arguments pro or against it.

What do other functions accept?

Depends on the function. 😉 For example, the Version.compare function allows several types. In that example, it is useful as developers might have different types to compare against it.

@tomschr
Copy link
Member

tomschr commented Nov 16, 2022

It seems to me, the is_compatible is basically an extended comparison like the tilde comparison:

~1.2.3 := >=1.2.3 and <2.0.0

Correct? In that case, this issue overlaps with PR #367 and it contains exactly that. Maybe in that PR we could rename the private Spec._tilde to Spec.is_compatible to make it more obvious.

The question could be here, if we want to have these (extended?) comparisons inside the Version class or outsource it to a Spec class. My only concern here is that we should follow some separation of concerns and to not make the Version class too big.

@rafalkrupinski
Copy link
Contributor

@tomschr

It seems to me, the is_compatible is basically an extended comparison like the tilde comparison:

Not exactly, as @Lexicality rightfully noted, the patch part is ignored.

The difference is that SemVer is more about the public API and less about implementation. The API must not change at all between different patch releases so caller v1.2.3 is fully compatible with API of callee v1.2.0 .
Patch actually describes a change of callee's behaviour (a bug fix).

In my use case (files with version), it's perfectly fine to ignore the patch version.
The generator program ('caller' in our vocabulary) doesn't expect any behaviour from the consuming program ('callee'), only that in will accept the file as long as it validates against the contract (API@version).

Of course there might be cases when patch should be checked. If so, how about adding a parameter check_patch=False"

@rafalkrupinski
Copy link
Contributor

rafalkrupinski commented Nov 16, 2022

@Lexicality

From what I understand, the current a.is_compatible(b) shows if you could replace package b with package a without any problems right?

I'm not sure thinking of packages is the right approach. SemVer is more about API compatibility.
Dependency X 1.1.0 is API-compatible with X 1.0.0 because caller using 1.0.0 API will work with X 1.1.0 on the API level.

But consider this:
Caller may depend on dependency X 1.1.1 because 1.1.0 has a bug that's fixed in 1.1.1
Alternatively, caller may depend on X 1.1.0's bug and break with 1.1.1
Those cases are beyond SemVer specification.

I think we're trying to handle two separate use cases:

  • I want to use version X, but not Y or Z (>=1.2.3 <2.0.0) - useful in packaging time dependency resolution.
  • Can I use version X, if I'm expecting version Y - useful at runtime, like for remote APIs, files or plug-ins.

The extended comparison would handle the former case, is_compatible would handle the latter.

@tlaferriere
Copy link
Contributor

From what I understand, the current a.is_compatible(b) shows if you could replace package b with package a without any problems right?
I don't think the is_breaking would reflect that behaviour at all?

Just so I understand

  • 1.2.0 is compatible with 1.1.1
  • 2.0.0 is not compatible with 1.1.1
  • 1.1.1 is compatible with 1.1.1
  • 1.1.0 is not compatible with 1.1.1

Right?

@Lexicality you are very right indeed. The direction of the relation I was talking about was not that of the dependency, but the bump (I totally misunderstood that). Therefore this would make more sense:

def is_compatible_with(self, new: Version) -> bool:
    ...

Or

def is_compatible(old: Version, new: Version):
    ...

So please disregard anything I said about dependency relationships, and replace it with old/new relationship instead 😅

@tomschr
Copy link
Member

tomschr commented Nov 16, 2022

Thank you for all your input. It gives me a much better picture.

However, still we need to solve the following questions:

  1. Should is_compatible() allow other types than Version?
  2. Should is_compatible() also check for patches, if we have an optional argument set check_patches=True as suggested by Raphael in Feature request: concise "compatibility" matching #284 (comment)?

tomschr added a commit to tomschr/python-semver that referenced this issue Nov 18, 2022
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 18, 2022
* Introduce Spec class to deal with such comparisons
* Improve documentation
* Simplify code in Version.match (delegates to Spec.match)
tomschr added a commit to tomschr/python-semver that referenced this issue Dec 20, 2022
* Implement Version.is_compatible() method
* Update test cases
* Update documentation
* Rename isvalid method to is_valid to make it consistent
  with is_compatible

The algorithm checks:

* if the two majors are different, it's incompatible
* if the two majors are equal, but the minor of the new
  version is lower than the old, it's incompatible
* if both prereleases are present and different,
  it's incompatible
* otherwise it's compatible

The algorithm does *not* check patches!

Co-authored-by: Lexi Robinson <lexi@lexi.org.uk>
Co-authored-by: Thomas Laferriere <tlaferriere@users.noreply.github.com>
Co-authored-by: Raphael Krupinski <rafalkrupinski@users.noreply.github.com>
tomschr added a commit to tomschr/python-semver that referenced this issue Dec 20, 2022
* Implement Version.is_compatible() method
* Update test cases
* Update documentation
* Rename isvalid method to is_valid to make it consistent
  with is_compatible

The algorithm checks:

* if the two majors are different, it's incompatible
* if the two majors are equal, but the minor of the new
  version is lower than the old, it's incompatible
* if both prereleases are present and different,
  it's incompatible
* otherwise it's compatible

The algorithm does *not* check patches!

Co-authored-by: Lexi Robinson <lexi@lexi.org.uk>
Co-authored-by: Thomas Laferriere <tlaferriere@users.noreply.github.com>
Co-authored-by: Raphael Krupinski <rafalkrupinski@users.noreply.github.com>
tomschr added a commit to tomschr/python-semver that referenced this issue Dec 22, 2022
* Implement Version.is_compatible() method
* Update test cases
* Update documentation
* Rename isvalid method to is_valid to make it consistent
  with is_compatible

The algorithm checks:

* if the two majors are different, it's incompatible
* if the two majors are equal, but the minor of the new
  version is lower than the old, it's incompatible
* if both prereleases are present and different,
  it's incompatible
* otherwise it's compatible

The algorithm does *not* check patches!

Co-authored-by: Lexi Robinson <lexi@lexi.org.uk>
Co-authored-by: Thomas Laferriere <tlaferriere@users.noreply.github.com>
Co-authored-by: Raphael Krupinski <rafalkrupinski@users.noreply.github.com>
tomschr added a commit to tomschr/python-semver that referenced this issue Dec 22, 2022
* Implement Version.is_compatible() method
* Update test cases
* Update documentation
* Rename isvalid method to is_valid to make it consistent
  with is_compatible

The result is True, if either of the following is true:

* both versions are equal, or
* both majors are equal and higher than 0. Same for both minors.
  Both pre-releases are equal, or
* both majors are equal and higher than 0. The minor of b's
  minor version is higher then a's. Both pre-releases are equal.

The algorithm does *not* check patches!

Co-authored-by: Lexi Robinson <lexi@lexi.org.uk>
Co-authored-by: Thomas Laferriere <tlaferriere@users.noreply.github.com>
Co-authored-by: Raphael Krupinski <rafalkrupinski@users.noreply.github.com>
tomschr added a commit to tomschr/python-semver that referenced this issue Dec 22, 2022
* Implement Version.is_compatible() method
* Update test cases
* Update documentation
* Rename isvalid method to is_valid to make it consistent
  with is_compatible

The result is True, if either of the following is true:

* both versions are equal, or
* both majors are equal and higher than 0. Same for both minors.
  Both pre-releases are equal, or
* both majors are equal and higher than 0. The minor of b's
  minor version is higher then a's. Both pre-releases are equal.

The algorithm does *not* check patches!

Co-authored-by: Lexi Robinson <lexi@lexi.org.uk>
Co-authored-by: Thomas Laferriere <tlaferriere@users.noreply.github.com>
Co-authored-by: Raphael Krupinski <rafalkrupinski@users.noreply.github.com>
tomschr added a commit to tomschr/python-semver that referenced this issue Dec 22, 2022
* Implement Version.is_compatible() method
* Update test cases
* Update documentation
* Rename isvalid method to is_valid to make it consistent
  with is_compatible

The result is True, if either of the following is true:

* both versions are equal, or
* both majors are equal and higher than 0. Same for both minors.
  Both pre-releases are equal, or
* both majors are equal and higher than 0. The minor of b's
  minor version is higher then a's. Both pre-releases are equal.

The algorithm does *not* check patches!

Co-authored-by: Lexi Robinson <lexi@lexi.org.uk>
Co-authored-by: Thomas Laferriere <tlaferriere@users.noreply.github.com>
Co-authored-by: Raphael Krupinski <rafalkrupinski@users.noreply.github.com>
tomschr added a commit to tomschr/python-semver that referenced this issue Dec 22, 2022
* Implement Version.is_compatible() method
* Update test cases
* Update documentation
* Rename isvalid method to is_valid to make it consistent
  with is_compatible

The result is True, if either of the following is true:

* both versions are equal, or
* both majors are equal and higher than 0. Same for both minors.
  Both pre-releases are equal, or
* both majors are equal and higher than 0. The minor of b's
  minor version is higher then a's. Both pre-releases are equal.

The algorithm does *not* check patches!

Co-authored-by: Lexi Robinson <lexi@lexi.org.uk>
Co-authored-by: Thomas Laferriere <tlaferriere@users.noreply.github.com>
Co-authored-by: Raphael Krupinski <rafalkrupinski@users.noreply.github.com>
@tomschr tomschr pinned this issue Mar 5, 2023
tomschr added a commit to tomschr/python-semver that referenced this issue Mar 5, 2023
tomschr added a commit to tomschr/python-semver that referenced this issue Mar 5, 2023
* Introduce Spec class to deal with such comparisons
* Improve documentation
* Simplify code in Version.match (delegates to Spec.match)
tomschr added a commit to tomschr/python-semver that referenced this issue Mar 5, 2023
tomschr added a commit to tomschr/python-semver that referenced this issue Mar 5, 2023
* Introduce Spec class to deal with such comparisons
* Improve documentation
* Simplify code in Version.match (delegates to Spec.match)
tomschr added a commit to tomschr/python-semver that referenced this issue Mar 5, 2023
* Introduce Spec class to deal with such comparisons
* Improve documentation
* Simplify code in Version.match (delegates to Spec.match)
@tomschr tomschr closed this as completed in 5485b6b Mar 7, 2023
tomschr added a commit that referenced this issue Mar 7, 2023
Fix #284: implement "is compatible with" method
tomschr added a commit to tomschr/python-semver that referenced this issue Mar 7, 2023
tomschr added a commit to tomschr/python-semver that referenced this issue Mar 7, 2023
* Introduce Spec class to deal with such comparisons
* Improve documentation
* Simplify code in Version.match (delegates to Spec.match)
@tomschr tomschr unpinned this issue Mar 7, 2023
tomschr added a commit to tomschr/python-semver that referenced this issue Jul 2, 2023
tomschr added a commit to tomschr/python-semver that referenced this issue Jul 2, 2023
* Introduce Spec class to deal with such comparisons
* Improve documentation
* Simplify code in Version.match (delegates to Spec.match)
tomschr added a commit to tomschr/python-semver that referenced this issue Jul 24, 2023
tomschr added a commit to tomschr/python-semver that referenced this issue Jul 24, 2023
* Introduce Spec class to deal with such comparisons
* Improve documentation
* Simplify code in Version.match (delegates to Spec.match)
tomschr added a commit to tomschr/python-semver that referenced this issue Sep 16, 2023
tomschr added a commit to tomschr/python-semver that referenced this issue Sep 16, 2023
* Introduce Spec class to deal with such comparisons
* Improve documentation
* Simplify code in Version.match (delegates to Spec.match)
tomschr added a commit to tomschr/python-semver that referenced this issue Oct 5, 2023
tomschr added a commit to tomschr/python-semver that referenced this issue Oct 5, 2023
* Introduce Spec class to deal with such comparisons
* Improve documentation
* Simplify code in Version.match (delegates to Spec.match)
tomschr added a commit to tomschr/python-semver that referenced this issue Nov 1, 2023
tomschr added a commit to tomschr/python-semver that referenced this issue Aug 13, 2024
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
None yet
Development

No branches or pull requests

5 participants