Skip to content
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

Add support for version compare and for checking for version part argument #20

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
*/**/.pytest_cache/
htmlcov/
.tox/
.coverage
Expand Down Expand Up @@ -129,3 +130,8 @@ Session.vim

# Auto-generated tag files
tags

\.idea/

\.DS_Store

123 changes: 110 additions & 13 deletions bumpversion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@


import argparse
from argparse import _AppendAction
import os
import re
import sre_constants
import subprocess
import warnings
import io
import operator
import logging
from string import Formatter
from datetime import datetime
from difflib import unified_diff
Expand All @@ -29,6 +32,7 @@
import codecs

from bumpversion.version_part import VersionPart, NumericVersionPartConfiguration, ConfiguredVersionPartConfiguration
from bumpversion.functions import NumericFunction

if sys.version_info[0] == 2:
sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
Expand All @@ -40,11 +44,11 @@
sys.version.split("\n")[0].split(" ")[0],
)

import logging

logger = logging.getLogger("bumpversion.logger")
logger_list = logging.getLogger("bumpversion.list")

from argparse import _AppendAction

class DiscardDefaultIfSpecifiedAppendAction(_AppendAction):

'''
Expand All @@ -59,11 +63,13 @@ def __call__(self, parser, namespace, values, option_string=None):
super(DiscardDefaultIfSpecifiedAppendAction, self).__call__(
parser, namespace, values, option_string=None)


time_context = {
'now': datetime.now(),
'utcnow': datetime.utcnow(),
}


class BaseVCS(object):

@classmethod
Expand Down Expand Up @@ -203,12 +209,14 @@ def tag(cls, sign, name, message):
command += ['--message', message]
subprocess.check_output(command)


VCS = [Git, Mercurial]


def prefixed_environ():
return dict((("${}".format(key), value) for key, value in os.environ.items()))


class ConfiguredFile(object):

def __init__(self, path, versionconfig):
Expand Down Expand Up @@ -301,30 +309,41 @@ def __str__(self):
def __repr__(self):
return '<bumpversion.ConfiguredFile:{}>'.format(self.path)


class IncompleteVersionRepresenationException(Exception):
def __init__(self, message):
self.message = message


class MissingValueForSerializationException(Exception):
def __init__(self, message):
self.message = message


class WorkingDirectoryIsDirtyException(Exception):
def __init__(self, message):
self.message = message


class MercurialDoesNotSupportSignedTagsException(Exception):
def __init__(self, message):
self.message = message


class UnkownPart(Exception):
def __init__(self, message):
self.message = message


def keyvaluestring(d):
return ", ".join("{}={}".format(k, v) for k, v in sorted(d.items()))


class Version(object):

def __init__(self, values, original=None):
def __init__(self, values, order):
self._values = dict(values)
self.original = original
self.order = order

def __getitem__(self, key):
return self._values[key]
Expand All @@ -336,7 +355,78 @@ def __iter__(self):
return iter(self._values)

def __repr__(self):
return '<bumpversion.Version:{}>'.format(keyvaluestring(self._values))
by_part = ", ".join("{}={}".format(k, v) for k, v in self.items())
return '<{}:{}>'.format(self.__class__, by_part)

def __hash__(self):
return hash(tuple((k, v) for k, v in self.items()))

def _compare(self, other, method, strict=True):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is really hard to understand at first sight. The strict parameter should be named something clearer, perhaps allow_equal?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I have changed the logic and improved the docs so the intent is more clear

"""
When comparing versions we need to compare the three parts before we can decide if there is a difference.
Non-strict comparators need to be treated differently as they can not fail if the initial parts are equal
:param other: the other Version
:param method: the compare method
:param strict: if the comparsion is strict
:return:
"""
if set(self.order).difference(other.order):
raise TypeError("Versions use different parts, cant compare them.")
for (x, y) in zip(self.values(), other.values()):
if x == y:
continue
else:
return method(x, y)
return not strict
# try:

#
# for vals in ((v, other[k]) for k, v in self.items()):
# if vals[0] == vals[1]:
# continue
# return method(vals[0], vals[1])
# return not strict
# except KeyError:
#

def __eq__(self, other):
if self is other:
return True
if not isinstance(other, Version):
return False
try:
return all(v == other[k] for k, v in self.items())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this ignore the case there len(self.items()) < len(other.items())?

Copy link
Author

@arcanefoam arcanefoam Jun 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, we could do a len compare first to make sure both have the same number of parts. I will add this.

Copy link
Author

@arcanefoam arcanefoam Jul 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought, I think equality should be strict, that is: 1.4 != 1.4.0, by assuming they are the same we ignore the version pattern defined by parse and serialize. If both versions where generated by bumpversion then this highlights that the user has an error in the part configuration.

except KeyError:
raise TypeError("Versions use different parts, cant compare them.")

def __ne__(self, other):
return not self == other

def __le__(self, other):
return self._compare(other, operator.lt, False)

def __ge__(self, other):
return self._compare(other, operator.gt, False)

def __lt__(self, other):
return self._compare(other, operator.lt)

def __gt__(self, other):
return self._compare(other, operator.gt)

def items(self):
for k in self.order:
try:
yield k, self._values[k]
except KeyError:
raise StopIteration

def values(self):
for k in self.order:
try:
yield self._values[k]
except KeyError:
raise StopIteration

def bump(self, part_name, order):
bumped = False
Expand All @@ -354,10 +444,11 @@ def bump(self, part_name, order):
else:
new_values[label] = self._values[label].copy()

new_version = Version(new_values)
new_version = Version(new_values, self.order)

return new_version


class VersionConfig(object):

"""
Expand Down Expand Up @@ -410,8 +501,7 @@ def parse(self, version_string):
for key, value in match.groupdict().items():
_parsed[key] = VersionPart(value, self.part_configs.get(key))


v = Version(_parsed, version_string)
v = Version(_parsed, [o for o in self.order()])

logger.info("Parsed the following values: %s" % keyvaluestring(v._values))

Expand Down Expand Up @@ -472,7 +562,6 @@ def _serialize(self, version, serialize_format, context, raise_if_incomplete=Fal

return serialized


def _choose_serialize_format(self, version, context):

chosen = None
Expand Down Expand Up @@ -504,6 +593,7 @@ def serialize(self, version, context):
# logger.info("Serialized to '{}'".format(serialized))
return serialized


OPTIONAL_ARGUMENTS_THAT_TAKE_VALUES = [
'--config-file',
'--current-version',
Expand Down Expand Up @@ -540,6 +630,7 @@ def split_args_in_optional_and_positional(args):

return (positionals, args)


def main(original_args=None):

positionals, args = split_args_in_optional_and_positional(
Expand Down Expand Up @@ -756,10 +847,16 @@ def main(original_args=None):
if not 'new_version' in defaults and known_args.current_version:
try:
if current_version and len(positionals) > 0:
logger.info("Attempting to increment part '{}'".format(positionals[0]))
new_version = current_version.bump(positionals[0], vc.order())
logger.info("Values are now: " + keyvaluestring(new_version._values))
defaults['new_version'] = vc.serialize(new_version, context)
part = positionals[0]
logger.info("Attempting to increment part '{}'".format(part))
if part in vc.order():
logger.info("Bumped part found in parse parts")
new_version = current_version.bump(part, vc.order())
logger.info("Values are now: " + keyvaluestring(new_version._values))
defaults['new_version'] = vc.serialize(new_version, context)
else:
logger.info("Bumped part not found in parse parts")
raise UnkownPart("Bumped part not found in parse parts.")
except MissingValueForSerializationException as e:
logger.info("Opportunistic finding of new_version failed: " + e.message)
except IncompleteVersionRepresenationException as e:
Expand Down
5 changes: 4 additions & 1 deletion bumpversion/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,11 @@ def __init__(self, values, optional_value=None, first_value=None):

def bump(self, value):
try:
return self._values[self._values.index(value)+1]
return self._values[self.index(value) + 1]
except IndexError:
raise ValueError(
"The part has already the maximum value among {} and cannot be bumped.".format(self._values))

def index(self, value):
return self._values.index(value)

35 changes: 32 additions & 3 deletions bumpversion/version_part.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from bumpversion.functions import NumericFunction, ValuesFunction
import operator


class PartConfiguration(object):
function_cls = NumericFunction
Expand Down Expand Up @@ -35,10 +37,8 @@ class VersionPart(object):

def __init__(self, value, config=None):
self._value = value

if config is None:
config = NumericVersionPartConfiguration()

self.config = config

@property
Expand All @@ -63,8 +63,37 @@ def __repr__(self):
self.value
)

def __hash__(self):
return hash(self.value)

def _compare(self, other, method):
if self.config.function_cls is not other.config.function_cls:
raise TypeError("Versions use different part specific configuration, cant compare them.")
if self.config.function_cls is NumericFunction:
return method(self.value, other.value)
else:
# Compare order
idx1 = self.config.function.index(self.value)
idx2 = other.config.function.index(other.value)
return method(idx1, idx2)

def __eq__(self, other):
return self.value == other.value
return self._compare(other, operator.eq)

def __lt__(self, other):
return self._compare(other, operator.lt)

def __le__(self, other):
return self._compare(other, operator.le)

def __ge__(self, other):
return self._compare(other, operator.ge)

def __gt__(self, other):
return self._compare(other, operator.gt)

def __ne__(self, other):
return not self == other

def null(self):
return VersionPart(self.config.first_value, self.config)
Loading