Skip to content

Commit

Permalink
Fix up
Browse files Browse the repository at this point in the history
  • Loading branch information
dvershinin committed Sep 16, 2023
1 parent 9629c83 commit d9246f6
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 52 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog
All notable changes to this project will be documented in this file.

## [3.1.1] - 2023-09-16
### Changed
* Some code refactoring and better identifying of pre-releases

## [3.1.1] - 2023-09-14
### Changed
* GitHub: when a semver version is detected, it is now used as a constraint #109
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ lastversion 1.2.3 -gt 1.2.4
#> 1.2.4
```

See exit codes below, to find whether the first argument is a higher version, or the second.
See the exit codes below, to find whether the first argument is a higher version, or the second.

The `--sem` option described earlier will affect both what's being printed and what semantic
versioning base level is being compared, thus the result.
Expand Down Expand Up @@ -584,12 +584,15 @@ fi

Exit status codes are the usual means of communicating a command's execution success or failure.
So `lastversion` follows this: successful command returns `0` while anything else is an error of
some kind:
some kind.
For example, when the latest stable release version if found, `0` is returned.
`0` is also returned for `-gt` comparison when leftmost argument is newer than rightmost argument.

Exit status code `1` is returned for cases like no release tag existing for repository at all, or
repository does not exist.

Exit status code `2` is returned for `-gt` version comparison negative lookup.
Exit status code `2` is returned for `-gt` version comparison negative lookup, that is when rightmost argument is newer
than leftmost argument.

Exit status code `3` is returned when filtering assets of last release yields empty URL set
(no match)
Expand Down
90 changes: 65 additions & 25 deletions lastversion/Version.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,57 @@ def is_semver(self):
"""Check if this a semantic version"""
return self.base_version.count('.') == 2

def __init__(self, version, char_fix_required=False):
"""Instantiate the `Version` object.
@staticmethod
def part_to_pypi(part):
"""
Convert a version part to a PyPI compatible string
See https://peps.python.org/pep-0440/
Helps devel releases to be correctly identified
See https://www.python.org/dev/peps/pep-0440/#developmental-releases
"""
if part in ['devel', 'test', 'dev']:
part = 'dev0'
elif part in ['alpha']:
# "4.3.0-alpha"
part = 'a0'
elif part in ['beta']:
# "4.3.0-beta"
part = 'b0'
# if part starts with rc<num>., discard non-relevant info while preserving RC level
elif re.search('^rc(\\d+)\\.', part):
# rc2.windows.1 => rc2.post1
sub_parts = part.split('.')
part = sub_parts[0]
for sub in sub_parts[1:]:
if sub.isdigit():
# use first numeric as post-release to RC
part += ".post" + sub
else:
# help post (patch) releases to be correctly identified (e.g. Magento 2.3.4-p2)
# p12 => post12
part = re.sub('^p(\\d+)$', 'post\\1', part, 1)
if part.isalpha():
# it's meaningless to us if it has only letters
part = None
return part

@staticmethod
def join_dashed_number_status(version):
"""
Join status with its number when separated by dash in a version string.
E.g., 4.27-chaos-preview-3 -> 4.27-chaos-pre3
Helps devel releases to be correctly identified
# https://www.python.org/dev/peps/pep-0440/#developmental-releases
Args:
version (str): The version-like string
char_fix_required (bool): Should we treat alphanumerics as part of version
version:
Returns:
str:
"""
self.fixed_letter_post_release = False
version = re.sub('-devel$', '-dev0', version, 1)
# help post (patch) releases to be correctly identified (e.g., Magento 2.3.4-p2)
version = re.sub('-p(\\d+)$', '-post\\1', version, 1)

# 4.27-chaos-preview-3 -> 4.27-chaos-pre3
version = re.sub('-preview-(\\d+)', '-pre\\1', version, 1)
Expand All @@ -47,32 +90,29 @@ def __init__(self, version, char_fix_required=False):
# v0.16.0-beta.rc4 -> v0.16.0-beta4
# both beta and rc? :) -> beta
version = re.sub(r'-beta[-.]rc(\d+)', '-beta\\1', version, 1)
return version

# many times they would tag foo-1.2.3 which would parse to LegacyVersion
# we can avoid this, by reassigning to what comes after the dash:
def __init__(self, version, char_fix_required=False):
"""Instantiate the `Version` object.
Args:
version (str): The version-like string
char_fix_required (bool): Should we treat alphanumerics as part of version
"""
self.fixed_letter_post_release = False

# Join status with its number when separated by dash in a version string, e.g., preview-3 -> pre3
version = self.join_dashed_number_status(version)

# parse out version components separated by dash
parts = version.split('-')

# TODO test v5.12-rc1-dontuse -> v5.12.rc1
# go through parts separated by dot, detect beta level, and weed out numberless info:
# go through parts which were separated by dash, detect beta level, and weed out numberless info:
parts_n = []
for part in parts:
# help devel releases to be correctly identified
# https://www.python.org/dev/peps/pep-0440/#developmental-releases
if part in ['devel', 'test', 'dev']:
part = 'dev0'
elif part in ['alpha']:
# "4.3.0-alpha"
part = 'a0'
elif part in ['beta']:
# "4.3.0-beta"
part = 'b0'
else:
# help post (patch) releases to be correctly identified (e.g. Magento 2.3.4-p2)
# p12 => post12
part = re.sub('^p(\\d+)$', 'post\\1', part, 1)
if not part.isalpha():
part = self.part_to_pypi(part)
if part:
parts_n.append(part)

if not parts_n:
raise InvalidVersion("Invalid version: '{0}'".format(version))
# remove *any* non-digits which appear at the beginning of the version string
Expand Down
2 changes: 1 addition & 1 deletion lastversion/__about__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version__ = '3.1.1'
__version__ = '3.2.0'
__self__ = "dvershinin/lastversion"
38 changes: 15 additions & 23 deletions lastversion/lastversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,31 +263,17 @@ def check_version(value):
In lastversion CLI app, this is used as argument parser helper for --newer-than (-gt) option.
Args:
value (str): Free-format string which is meant to contain user-supplied version
value (str): Free-format string which is meant to contain a user-supplied version
Raises:
argparse.ArgumentTypeError: Exception in case version was not found in the input string
argparse.ArgumentTypeError: Exception in a case version was not found in the input string
Returns:
Version: Parsed version object
"""
"""
Argument parser helper for --newer-than (-gt) option
:param value:
:type value:
:return:
:rtype:
"""
try:
# TODO use sanitize_version so that we can just pass tags as values
# help devel releases to be correctly identified
# https://www.python.org/dev/peps/pep-0440/#developmental-releases
value = re.sub('-devel$', '.dev0', value, 1)
# help post (patch) releases to be correctly identified (e.g. Magento 2.3.4-p2)
value = re.sub('-p(\\d+)$', '.post\\1', value, 1)
value = Version(value)
except InvalidVersion:
value = parse_version(value)
if not value:
raise argparse.ArgumentTypeError("%s is an invalid version value" % value)
return value

Expand Down Expand Up @@ -386,9 +372,15 @@ def install_app_image(url, install_name):
extract_appimage_desktop_file(app_file_name)


def main():
"""The entrypoint to CLI app."""
def main(argv=None):
"""
The entrypoint to CLI app.
Args:
argv: List of arguments, helps test CLI without resorting to subprocess module.
"""
epilog = None

if "GITHUB_API_TOKEN" not in os.environ and "GITHUB_TOKEN" not in os.environ:
epilog = TOKEN_PRO_TIP
parser = argparse.ArgumentParser(description='Find the latest software release.',
Expand Down Expand Up @@ -453,7 +445,7 @@ def main():
pre=False, assets=False, newer_than=False, filter=False,
shorter_urls=False, major=None, assumeyes=False, at=None,
having_asset=None, even=False)
args = parser.parse_args()
args = parser.parse_args(argv)

if args.repo == "self":
args.repo = __self__
Expand Down Expand Up @@ -510,7 +502,7 @@ def main():
print("Stable: {}".format(not v.is_prerelease))
else:
print(v)
sys.exit(0)
return sys.exit(0)

if args.action == 'install':
# we can only install assets
Expand Down Expand Up @@ -550,7 +542,7 @@ def main():
base_compare = parse_version(args.repo)
if base_compare:
print(max([args.newer_than, base_compare]))
sys.exit(2 if base_compare <= args.newer_than else 0)
return sys.exit(2 if base_compare <= args.newer_than else 0)

# other action are either getting release or doing something with release (extend get action)
try:
Expand Down
103 changes: 103 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import subprocess
import sys

from packaging import version
from contextlib import contextmanager
from lastversion import main


@contextmanager
def captured_exit_code():
"""Capture the exit code of a function."""
exit_code = None

def mock_exit(code=0):
"""Mock the exit function."""
nonlocal exit_code
exit_code = code

original_exit = sys.exit
sys.exit = mock_exit
try:
yield lambda: exit_code
finally:
sys.exit = original_exit


def test_cli_format_devel():
"""
Test that the CLI formatting returns the correct version for a devel version.
`lastversion test 'blah-1.2.3-devel' # > 1.2.3.dev0`
"""
process = subprocess.Popen(
['lastversion', 'format', 'blah-1.2.3-devel'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
out, err = process.communicate()

assert version.parse(out.decode('utf-8').strip()) == version.parse("1.2.3.dev0")


def test_cli_format_no_clear():
"""
Test that the CLI formatting returns error for a version that is not clear.
`lastversion test '1.2.x' # > False (no clear version)`
"""
process = subprocess.Popen(
['lastversion', 'format', '1.2.x'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
out, err = process.communicate()

# exit code should be 1
assert process.returncode == 1


def test_cli_format_rc1():
"""
Test that the CLI formatting returns the correct version for a rc version.
`lastversion test '1.2.3-rc1' # > 1.2.3rc1`
"""
process = subprocess.Popen(
['lastversion', 'format', '1.2.3-rc1'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
out, err = process.communicate()

assert version.parse(out.decode('utf-8').strip()) == version.parse("1.2.3-rc1")


def test_cli_format_rc_with_garbage(capsys):
"""Test that the CLI formatting returns the correct version for a rc version."""
with captured_exit_code() as get_exit_code:
main(['format', 'v5.12-rc1-dontuse'])
exit_code = get_exit_code()

captured = capsys.readouterr()
assert "5.12rc1" == captured.out.rstrip()
assert not exit_code # Check the exit code is correct


def test_cli_format_rc_with_post(capsys):
"""Test that the CLI formatting returns the correct version for a rc version."""
with captured_exit_code() as get_exit_code:
main(['format', 'v2.41.0-rc2.windows.1'])
exit_code = get_exit_code()

captured = capsys.readouterr()
assert "2.41.0rc2.post1" == captured.out.rstrip()
assert not exit_code # Check the exit code is correct


def test_cli_gt_stable_vs_rc(capsys):
"""Test that the CLI comparison is positive when comparing stable to RC."""
with captured_exit_code() as get_exit_code:
main(['v2.41.0.windows.1', '-gt', 'v2.41.0-rc2.windows.1'])
exit_code = get_exit_code()

captured = capsys.readouterr()
assert "2.41.0" == captured.out.rstrip()
assert not exit_code # Check the exit code is correct

0 comments on commit d9246f6

Please sign in to comment.