bump2version icon indicating copy to clipboard operation
bump2version copied to clipboard

Don't replace version numbers of *partial* matches

Open Sytten opened this issue 5 years ago • 10 comments

Given a setup.py file containing those two lines:

if pkg_resources.get_distribution("setuptools").version < "40.1.1":

# ... other lines

    version="0.1.1",

If you bump the version (the patch for example), it will modify 40.1.1 to 40.1.2. This is because it's a simple replace using the current version. It probably should use a regex to find the version the file. The only work around I found was to the change the search to include the double quotes.

Sytten avatar May 31 '19 16:05 Sytten

How does adding double quotes to search help here? 40.1.1 and 0.1.1 both have double quotes around them...

Was your intent to bump 40.1.1 to 40.1.2 or to bump 0.1.1 to 0.1.2 ?

Can you show your bumpversion configuration file and the command you use?

florisla avatar Jan 13 '20 09:01 florisla

This has been a while and I don't have access to the code anymore (proprietary). The goal was to bump 0.1.1 to 0.1.2 without changing the 40.1.1. In this example the double quotes help because if you only search for 0.1.1 it will find 2 elements 4[0.1.1] and [0.1.1]. If you search for "0.1.1", it will only match the version number (that we want to modify).

Sytten avatar Jan 13 '20 15:01 Sytten

I see, you got an accidental match.

The --current-version argument can't protect against that.

For future reference, you can use a custom search that only matches what you want:

[bumpversion]
current_version = 0.1.1

[bumpversion:file:setup.py]
search = "{current_version}"
replace = "{new_version}"

florisla avatar Jan 13 '20 20:01 florisla

I know, but I don't agree with your solution. If the regex for search was correct it wouldn't match 40 with 0 and thus not replace the version (for that I believe it needs to have a greedy search).

Sytten avatar Jan 14 '20 15:01 Sytten

Got it, the search matches 40.1.1 and 0.1.1; only the latter is recognized as the current version number. But the naive replacement code does two replacements instead of one.

florisla avatar Jan 14 '20 15:01 florisla

Yes exactly! It's probably not a big change, but we need to ensure backward compatibility with tests.

Sytten avatar Jan 15 '20 18:01 Sytten

This is not very trivial to fix since the current utils.ConfiguredFile.replace() does not use regular expressions (re.sub). It's simply serializing the search and replace text, and doing str.replace on the whole file content.

florisla avatar Jan 27 '20 14:01 florisla

I'm not working on a fix, but here is a unit test which demonstrates the problem.

https://github.com/florisla/bump2version/tree/dont-replace-partial-matches

florisla avatar Jan 27 '20 21:01 florisla

I'm trying to use re.sub and adding leading and trailing \b (word boundary) in the expression.

This works fine preventing the issue, but it has nasty side effect.

Previously, it would happily replace 'v0.1.1' with 'v0.1.2'. The word boundary checking prevents that (it does not detect a word boundary before 0), and requires a custom search and replace with the v prefix defined.

So this would be a breaking change.

Edit: Another special-case is needed to handle search expressions containing newlines. In that case, we can drop the word boundary check altogether.

florisla avatar Jan 27 '21 15:01 florisla

Maybe I miss some context, but what about matching [^\d] ("not a decimal digit", same as \D), instead of \b (word boundary)? In addition, accept start of line and end of string, in case there is no prefix/suffix. However, the smart handling of digits must be skipped if the version starts/ends with non-digits (e.g. -dev)

For example:

def ver_replace(current_version, new_version, string):
    repl = r'\g<prefix>' + new_version + r'\g<suffix>'
    if current_version[0].isdecimal():
        prefix = r'(?P<prefix>\D|^)'
    else:
        prefix = r'(?P<prefix>)'
    if current_version[-1].isdecimal():
        suffix = r'(?P<suffix>\D|$)'
    else:
        suffix = r'(?P<suffix>)'
    current_version = re.escape(current_version)
    pattern = prefix + current_version + suffix
    return re.sub(pattern, repl, string)
>>> ver_replace('1.2.3', '1.2.4', r'1.2.3')
'1.2.4'          # correctly replaced
>>> ver_replace('1.2.3', '1.2.4', r'v1.2.3')
'v1.2.4'         # correctly replaced with prefix
>>> ver_replace('1.2.3', '1.2.4', r'vvvv1.2.3')
'vvvv1.2.4'    # correctly replaced with multi-char prefix
>>> ver_replace('1.2.3', '1.2.4', r'91.2.3')
'91.2.3'         # correctly NOT replaced as it is wrong version
>>> ver_replace('1.2.3', '1.2.4', r'1.2.33')
'1.2.33'         # correctly NOT replaced as it is wrong version
>>> ver_replace('1.2.3', '1.2.4', r'1.2.3v')
'1.2.4v'         # correctly replaced even with suffix
>>> ver_replace('1.2.3', '1.2.4', r'abc\n1.2.3\ndef')
'abc\\n1.2.4\\ndef'  # correctly replaced even in case of newline
>>> ver_replace('1.2.3-dev', '1.2.4-dev', r'abc\n1.2.3-devX')
'abc\\n1.2.4-devX'   # correctly replaced with non-digit end of search string
>>> ver_replace('1.2.3-dev', '1.2.4-dev', r'abc\n1.2.3-dev0')
'abc\\n1.2.4-dev0'   # correctly replaced with non-digit end of search string
>>> ver_replace('X1.2.3', 'X1.2.4', r'abc\n0X1.2.3Y')
'abc\\n0X1.2.4Y'     # correctly replaced with non-digit start of search string

panicgh avatar Mar 25 '22 19:03 panicgh