Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fix #284: implement "is compatible with" method #368

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

Merged
merged 1 commit into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,6 @@ fabric.properties

docs/_api
!docs/_api/semver.__about__.rst

# For node
node_modules/
5 changes: 5 additions & 0 deletions changelog.d/284.deprecation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Deprecate the use of :meth:`Version.isvalid`.

Rename :meth:`Version.isvalid <semver.version.Version.isvalid>`
to :meth:`Version.is_valid <semver.version.Version.is_valid>`
for consistency reasons with :meth:`Version.is_compatible <semver.version.Version.is_compatible>`
1 change: 1 addition & 0 deletions changelog.d/284.doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Document deprecation of :meth:`Version.isvalid`.
1 change: 1 addition & 0 deletions changelog.d/284.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement :meth:`Version.is_compatible <semver.version.Version.is_compatible>` to make "is self compatible with X".
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ def find_version(*file_paths):
# Markup to shorten external links
# See https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html
extlinks = {
"gh": ("https://github.com/python-semver/python-semver/issues/%s", "#"),
"pr": ("https://github.com/python-semver/python-semver/pull/%s", "PR #"),
"gh": ("https://github.com/python-semver/python-semver/issues/%s", "#%s"),
"pr": ("https://github.com/python-semver/python-semver/pull/%s", "PR #%s"),
}

# -- Options for HTML output ----------------------------------------------
Expand Down
12 changes: 10 additions & 2 deletions docs/migration/migratetosemver3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
Migrating from semver2 to semver3
=================================

This document describes the visible differences for
This section describes the visible differences for
users and how your code stays compatible for semver3.
Some changes are backward incompatible.

Although the development team tries to make the transition
to semver3 as smooth as possible, at some point change
Expand Down Expand Up @@ -34,9 +35,16 @@ Use semver.cli instead of semver
--------------------------------

All functions related to CLI parsing are moved to :mod:`semver.cli`.
If you are such functions, like :func:`semver.cmd_bump <semver.cli.cmd_bump>`,
If you need such functions, like :func:`semver.cmd_bump <semver.cli.cmd_bump>`,
import it from :mod:`semver.cli` in the future:

.. code-block:: python

from semver.cli import cmd_bump


Use semver.Version.is_valid instead of semver.Version.isvalid
-------------------------------------------------------------

The pull request :pr:`284` introduced the method :meth:`Version.is_compatible <semver.Version.is_compatible>`. To keep consistency, the development team
decided to rename the :meth:`isvalid <semver.Version.isvalid>` to :meth:`is_valid <semver.Version.is_valid>`.
5 changes: 5 additions & 0 deletions docs/migration/replace-deprecated-functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ them with code which is compatible for future versions:

Likewise with the other module level functions.

* :func:`semver.Version.isvalid`

Replace it with :meth:`semver.Version.is_valid`:


* :func:`semver.finalize_version`

Replace it with :func:`semver.Version.finalize_version`:
Expand Down
95 changes: 95 additions & 0 deletions docs/usage/check-compatible-semver-version.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
Checking for a Compatible Semver Version
========================================

To check if a *change* from a semver version ``a`` to a semver
version ``b`` is *compatible* according to semver rule, use the method
:meth:`Version.is_compatible <semver.version.Version.is_compatible>`.

The expression ``a.is_compatible(b) is True`` if one of the following
statements is true:

* both versions are equal, or
* both majors are equal and higher than 0. The same applies for both
minor parts. 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.

In all other cases, the result is false.

Keep in mind, the method *does not* check patches!


* Two different majors:

.. code-block:: python

>>> a = Version(1, 1, 1)
>>> b = Version(2, 0, 0)
>>> a.is_compatible(b)
False
>>> b.is_compatible(a)
False

* Two different minors:

.. code-block:: python

>>> a = Version(1, 1, 0)
>>> b = Version(1, 0, 0)
>>> a.is_compatible(b)
False
>>> b.is_compatible(a)
True

* The same two majors and minors:

.. code-block:: python

>>> a = Version(1, 1, 1)
>>> b = Version(1, 1, 0)
>>> a.is_compatible(b)
True
>>> b.is_compatible(a)
True

* Release and pre-release:

.. code-block:: python

>>> a = Version(1, 1, 1)
>>> b = Version(1, 0, 0,'rc1')
>>> a.is_compatible(b)
False
>>> b.is_compatible(a)
False

* Different pre-releases:

.. code-block:: python

>>> a = Version(1, 0, 0, 'rc1')
>>> b = Version(1, 0, 0, 'rc2')
>>> a.is_compatible(b)
False
>>> b.is_compatible(a)
False

* Identical pre-releases:

.. code-block:: python

>>> a = Version(1, 0, 0,'rc1')
>>> b = Version(1, 0, 0,'rc1')
>>> a.is_compatible(b)
True

* All major zero versions are incompatible with anything but itself:

.. code-block:: python

>>> Version(0, 1, 0).is_compatible(Version(0, 1, 1))
False

# Only identical versions are compatible for major zero versions:
>>> Version(0, 1, 0).is_compatible(Version(0, 1, 0))
True
4 changes: 2 additions & 2 deletions docs/usage/check-valid-semver-version.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ classmethod :func:`Version.isvalid <semver.version.Version.isvalid>`:

.. code-block:: python

>>> Version.isvalid("1.0.0")
>>> Version.is_valid("1.0.0")
True
>>> Version.isvalid("invalid")
>>> Version.is_valid("invalid")
False
1 change: 1 addition & 0 deletions docs/usage/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Using semver
create-a-version
parse-version-string
check-valid-semver-version
check-compatible-semver-version
access-parts-of-a-version
access-parts-through-index
replace-parts-of-a-version
Expand Down
2 changes: 1 addition & 1 deletion src/semver/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def cmd_check(args: argparse.Namespace) -> None:

:param args: The parsed arguments
"""
if Version.isvalid(args.version):
if Version.is_valid(args.version):
return None
raise ValueError("Invalid version %r" % args.version)

Expand Down
38 changes: 37 additions & 1 deletion src/semver/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ def replace(self, **parts: Union[int, Optional[str]]) -> "Version":
raise TypeError(error)

@classmethod
def isvalid(cls, version: str) -> bool:
def is_valid(cls, version: str) -> bool:
"""
Check if the string is a valid semver version.

Expand All @@ -663,6 +663,42 @@ def isvalid(cls, version: str) -> bool:
except ValueError:
return False

def is_compatible(self, other: "Version") -> bool:
"""
Check if current version is compatible with other version.

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.

:param other: the version to check for compatibility
:return: True, if ``other`` is compatible with the old version,
otherwise False

>>> Version(1, 1, 0).is_compatible(Version(1, 0, 0))
False
>>> Version(1, 0, 0).is_compatible(Version(1, 1, 0))
True
"""
if not isinstance(other, Version):
raise TypeError(f"Expected a Version type but got {type(other)}")

# All major-0 versions should be incompatible with anything but itself
if (0 == self.major == other.major) and (self[:4] != other[:4]):
return False

return (
(self.major == other.major)
and (other.minor >= self.minor)
and (self.prerelease == other.prerelease)
)


#: Keep the VersionInfo name for compatibility
VersionInfo = Version
59 changes: 57 additions & 2 deletions tests/test_semver.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,65 @@ def test_should_be_able_to_use_integers_as_prerelease_build():


def test_should_versioninfo_isvalid():
assert Version.isvalid("1.0.0") is True
assert Version.isvalid("foo") is False
assert Version.is_valid("1.0.0") is True
assert Version.is_valid("foo") is False


def test_versioninfo_compare_should_raise_when_passed_invalid_value():
with pytest.raises(TypeError):
Version(1, 2, 3).compare(4)


@pytest.mark.parametrize(
"old, new",
[
((1, 2, 3), (1, 2, 3)),
((1, 2, 3), (1, 2, 4)),
((1, 2, 4), (1, 2, 3)),
((1, 2, 3, "rc.0"), (1, 2, 4, "rc.0")),
((0, 1, 0), (0, 1, 0)),
],
)
def test_should_succeed_compatible_match(old, new):
old = Version(*old)
new = Version(*new)
assert old.is_compatible(new)


@pytest.mark.parametrize(
"old, new",
[
((1, 1, 0), (1, 0, 0)),
((2, 0, 0), (1, 5, 0)),
((1, 2, 3, "rc.1"), (1, 2, 3, "rc.0")),
((1, 2, 3, "rc.1"), (1, 2, 4, "rc.0")),
((0, 1, 0), (0, 1, 1)),
((1, 0, 0), (1, 0, 0, "rc1")),
((1, 0, 0, "rc1"), (1, 0, 0)),
],
)
def test_should_fail_compatible_match(old, new):
old = Version(*old)
new = Version(*new)
assert not old.is_compatible(new)


@pytest.mark.parametrize(
"wrongtype",
[
"wrongtype",
dict(a=2),
list(),
],
)
def test_should_fail_with_incompatible_type_for_compatible_match(wrongtype):
with pytest.raises(TypeError, match="Expected a Version type .*"):
v = Version(1, 2, 3)
v.is_compatible(wrongtype)


def test_should_succeed_with_compatible_subclass_for_is_compatible():
class CustomVersion(Version):
...

assert CustomVersion(1, 0, 0).is_compatible(Version(1, 0, 0))
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ python =

[testenv]
description = Run test suite for {basepython}
allowlist_externals = make
skip_install = true
allowlist_externals = make
commands = pytest {posargs:}
deps =
pytest
Expand Down