Repository: c4urself/bump2version

Issue:
Support processing a file multiple times
For a certain file I needed to have two different serializations.

Turns out you can do that using `bumpversion.cfg` as long as the file name is written differently each time.  I'm using a `./` prefix here.

    [bumpversion:file:version.txt]
    search = Version: {current_version}
    replace = Version: {new_version}
    parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
    serialize = {major}.{minor}.{patch}

    [bumpversion:file:./version.txt]
    search = Release: {current_version}
    replace= Release: {new_version}
    parse = (?P<major>\d+)-(?P<minor>\d+)-(?P<patch>\d+)
    serialize = {major}-{minor}-{patch}

Is this something we should document?  I *think* this can solve #43 and #4, although it's perhaps not a very intuitive solution for those issues.

Thought: we could extend the `[bumpversion:file]` syntax to support multiple editions to make this more readable, like

    [bumpversion:file:version.txt:version]
    ...
    [bumpversion:file:version.txt:release]
    ....

Discussion:
florisla: Only failing on Python 2; I'll rebase after 2.x is dropped.
florisla: File names can contain colons.

So using an underscore would break functionality if users actually have such file names.

That's why I went for the underscore approach.

Not sure if there is a better way...
ekohl: I guess one way to parse it is:
```python
section_name = 'bumpversion:file_identifier:path/to/file'
try:
    _, section_prefix, section_value = section_name_match.split(':', 3)
except ValueError:
    continue

if section_prefix == 'file' or section_prefix.startswith('file_'):
    pass
```

I also thought about `;` (so `file;identifier`) but `:` and `;` look a lot alike.
c4urself: -- there's also the json variant that's being proposed which is `file+json:<filename>` (https://github.com/c4urself/bump2version/pull/48/files)

I feel it may be (slightly) more readable to say `file+file1` and `file+file2` where the syntax is `<type>+<identifier>` and the json one could just be changed to `json(+<identifier>)?:<filename>` -- thoughts?
florisla: I would rename `file+json` from `#48` to `jsonfile`.

Agreed on the underscore not being a clear separator.  Perhaps a `#`hash is clear indication that it's a non-functional suffix?

    [bumpversion:file#first:path]
    [bumpversion:file#second:path]

I'll test if this breaks the INI parser.
florisla: Hashes are possible, but not beautiful.

    # regular
    [bumpversion:file:myfile.txt]
    # underscore is not clear
    [bumpversion:file_customized:myfile.txt]
    # hash is ugly
    [bumpversion:file#customized:myfile.txt]
    # brackets -- too many brackes
    [bumpversion:file[customized]:myfile.txt]
    # braces -- looks like a format string
    [bumpversion:file{customized}:myfile.txt]

I like how parens convey that the suffix contain optional information.

    [bumpversion:file(customized):myfile.txt]
    [bumpversion:file ( really customized ):myfile.txt]

ekohl: @florisla do you want to merge this as is, squash on merge or manually change commits for a cleaner history? Either way, it looks like there's consensus on merging this.
florisla: Thanks for reviewing! I'll rebase, squash and merge.

Patch:
diff --git a/README.md b/README.md
index 4221d82..4f6fbf3 100644
--- a/README.md
+++ b/README.md
@@ -298,6 +298,11 @@ values =
 
 This configuration is in the section: `[bumpversion:file:…]`
 
+Note: The configuration file format requires each section header to be
+unique. If you want to process a certain file multiple times,
+you may append a description between parens to the `file` keyword:
+`[bumpversion:file (special one):…]`.
+
 #### `parse =`
   **default:** `(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)`
 
diff --git a/bumpversion/cli.py b/bumpversion/cli.py
index ea641de..762ec45 100644
--- a/bumpversion/cli.py
+++ b/bumpversion/cli.py
@@ -42,6 +42,16 @@ DESCRIPTION = "{}: v{} (using Python v{})".format(
 )
 VCS = [Git, Mercurial]
 
+# detect either
+# bumpversion:part:value
+# bumpversion:file:value
+# bumpversion:file(suffix):value
+# bumpversion:file ( suffix with spaces):value
+RE_DETECT_SECTION_TYPE = re.compile(
+    r"^bumpversion:"
+    r"((?P<file>file)(\s*\(\s*(?P<file_suffix>[^\):]+)\)?)?|(?P<part>part)):"
+    r"(?P<value>.+)",
+)
 
 logger_list = logging.getLogger("bumpversion.list")
 logger = logging.getLogger(__name__)
@@ -279,18 +289,18 @@ def _load_configuration(config_file, explicit_config, defaults):
 
     part_configs = {}
     files = []
-    file_or_part = re.compile("^bumpversion:(file|part):(.+)")
+
     for section_name in config.sections():
-        section_name_match = file_or_part.match(section_name)
+        section_type_match = RE_DETECT_SECTION_TYPE.match(section_name)
 
-        if not section_name_match:
+        if not section_type_match:
             continue
 
-        section_prefix, section_value = section_name_match.groups()
-
+        section_type = section_type_match.groupdict()
+        section_value = section_type.get("value")
         section_config = dict(config.items(section_name))
 
-        if section_prefix == "part":
+        if section_type.get("part"):
             ThisVersionPartConfiguration = NumericVersionPartConfiguration
 
             if "values" in section_config:
@@ -305,8 +315,7 @@ def _load_configuration(config_file, explicit_config, defaults):
             part_configs[section_value] = ThisVersionPartConfiguration(
                 **section_config
             )
-
-        elif section_prefix == "file":
+        elif section_type.get("file"):
             filename = section_value
 
             if "serialize" in section_config:


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 71ef3a0..b1bc1a4 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -64,6 +64,17 @@ def configfile(request):
     return request.param
 
 
+@pytest.fixture(params=[
+    "file",
+    "file(suffix)",
+    "file (suffix with space)",
+    "file (suffix lacking closing paren",
+])
+def file_keyword(request):
+    """Return multiple possible styles for the bumpversion:file keyword."""
+    return request.param
+
+
 try:
     RawConfigParser(empty_lines_in_values=False)
     using_old_configparser = False
@@ -281,6 +292,22 @@ new_version: 0.10.3
     assert "0.10.3" == tmpdir.join("file2").read()
 
 
+def test_file_keyword_with_suffix_is_accepted(tmpdir, configfile, file_keyword):
+    tmpdir.join("file2").write("0.10.2")
+    tmpdir.join(configfile).write(
+        """[bumpversion]
+    current_version: 0.10.2
+    new_version: 0.10.3
+    [bumpversion:%s:file2]
+    """ % file_keyword
+    )
+
+    tmpdir.chdir()
+    main(['patch'])
+
+    assert "0.10.3" == tmpdir.join("file2").read()
+
+
 def test_multiple_config_files(tmpdir):
     tmpdir.join("file2").write("0.10.2")
     tmpdir.join("setup.cfg").write("""[bumpversion]
@@ -298,6 +325,39 @@ new_version: 0.10.4
     assert "0.10.4" == tmpdir.join("file2").read()
 
 
+def test_single_file_processed_twice(tmpdir):
+    """
+    Verify that a single file "file2" can be processed twice.
+
+    Use two file_ entries, both with a different suffix after
+    the underscore.
+    Employ different parse/serialize and search/replace configs
+    to verify correct interpretation.
+    """
+    tmpdir.join("file2").write("dots: 0.10.2\ndashes: 0-10-2")
+    tmpdir.join("setup.cfg").write("""[bumpversion]
+current_version: 0.10.2
+new_version: 0.10.3
+[bumpversion:file:file2]""")
+    tmpdir.join(".bumpversion.cfg").write(r"""[bumpversion]
+current_version: 0.10.2
+new_version: 0.10.4
+[bumpversion:file (version with dots):file2]
+search = dots: {current_version}
+replace = dots: {new_version}
+[bumpversion:file (version with dashes):file2]
+search = dashes: {current_version}
+replace = dashes: {new_version}
+parse = (?P<major>\d+)-(?P<minor>\d+)-(?P<patch>\d+)
+serialize = {major}-{minor}-{patch}
+""")
+
+    tmpdir.chdir()
+    main(['patch'])
+
+    assert "dots: 0.10.4\ndashes: 0-10-4" == tmpdir.join("file2").read()
+
+
 def test_config_file_is_updated(tmpdir):
     tmpdir.join("file3").write("0.0.13")
     tmpdir.join(".bumpversion.cfg").write("""[bumpversion]