Repository: relekang/python-semantic-release (python)

Issue:
Unable to specify version_pattern with start/end of line (^ or $)
## The problem

`version_pattern` does not allow for the use of `^` or `$` to signify the start or end of a line. This means that some unwanted matches might be found (examples below)

## Expected behavior

Using `^` or `$` would match the start or end of a line in the target file, as in `multiline` regular expressions.

## Additional context

`pyproject.toml`
```toml
[tool.poetry]
version = "0.1.0"

[tool.semantic_release]
version_pattern = 'pyproject.toml:version = "{version}"'

[tool.pytest.ini_options]
minversion = "6.0"
```

Running `semantic-release --noop version` results in the following:
```console
$ semantic-release --noop version
Creating new version
error: found conflicting versions: '0.1.0', '6.0'
```

It appears this is due to the use of `re.finditer` in [VersionPattern.parse](https://github.com/relekang/python-semantic-release/blob/34a2b2e91c2439fec7f08bfc628d7f470ef2fdfe/semantic_release/history/__init__.py#L74).

## Workarounds

There is a simple way to work around this limitation: specify the inline multiline flag `(?m)` somewhere in the pattern string, e.g.:

```toml
version_pattern = 'pyproject.toml:version = "{version}"(?m)'
```

However, given the use cases for this tool and the places where the version is likely to be, it seems like it might be good to turn on the multiline flag by default.

Pull request:
Support multiline version_pattern matching by default
Closes #258 

This enables `re.MULTILINE` matching for `version_pattern` specifiers by default.

It could be argued that this is actually not a fix but a breaking change, as technically using `^` or `$` would match the start/end of the entire file contents with the current version of code, but in practice I doubt this is likely to be the case.

Patch:
diff --git a/semantic_release/history/__init__.py b/semantic_release/history/__init__.py
index 2ca1ae0..516b685 100644
--- a/semantic_release/history/__init__.py
+++ b/semantic_release/history/__init__.py
@@ -24,9 +24,9 @@ class VersionPattern:
     """
     Represent a version number in a particular file.
 
-    The version number is identified by a regular expression.  Methods are 
-    provided both the read the version number from the file, and to update the 
-    file with a new version number.  Use the `load_version_patterns()` factory 
+    The version number is identified by a regular expression.  Methods are
+    provided both the read the version number from the file, and to update the
+    file with a new version number.  Use the `load_version_patterns()` factory
     function to create the version patterns specified in the config files.
     """
 
@@ -41,7 +41,7 @@ class VersionPattern:
     @classmethod
     def from_variable(cls, config_str: str):
         """
-        Instantiate a `VersionPattern` from a string specifying a path and a 
+        Instantiate a `VersionPattern` from a string specifying a path and a
         variable name.
         """
         path, variable = config_str.split(":", 1)
@@ -51,7 +51,7 @@ class VersionPattern:
     @classmethod
     def from_pattern(cls, config_str: str):
         """
-        Instantiate a `VersionPattern` from a string specifying a path and a 
+        Instantiate a `VersionPattern` from a string specifying a path and a
         regular expression matching the version number.
         """
         path, pattern = config_str.split(":", 1)
@@ -62,16 +62,16 @@ class VersionPattern:
         """
         Return the versions matching this pattern.
 
-        Because a pattern can match in multiple places, this method returns a 
-        set of matches.  Generally, there should only be one element in this 
-        set (i.e. even if the version is specified in multiple places, it 
-        should be the same version in each place), but it falls on the caller 
+        Because a pattern can match in multiple places, this method returns a
+        set of matches.  Generally, there should only be one element in this
+        set (i.e. even if the version is specified in multiple places, it
+        should be the same version in each place), but it falls on the caller
         to check for this condition.
         """
         with open(self.path, "r") as f:
             content = f.read()
 
-        versions = {m.group(1) for m in re.finditer(self.pattern, content)}
+        versions = {m.group(1) for m in re.finditer(self.pattern, content, re.MULTILINE)}
 
         logger.debug(
             f"Parsing current version: path={self.path!r} pattern={self.pattern!r} num_matches={len(versions)}"
@@ -82,7 +82,7 @@ class VersionPattern:
         """
         Update the versions matching this pattern.
 
-        This method reads the underlying file, replaces each occurrence of the 
+        This method reads the underlying file, replaces each occurrence of the
         matched pattern, then writes the updated file.
 
         :param new_version: The new version number as a string
@@ -130,7 +130,7 @@ def get_current_version_by_config_file() -> str:
     Get current version from the version variable defined in the configuration.
 
     :return: A string with the current version number
-    :raises ImproperConfigurationError: if either no versions are found, or 
+    :raises ImproperConfigurationError: if either no versions are found, or
     multiple versions are found.
     """
     patterns = load_version_patterns()


Test patch:
diff --git a/tests/history/test_version.py b/tests/history/test_version.py
index 38b8f62..4d4bb00 100644
--- a/tests/history/test_version.py
+++ b/tests/history/test_version.py
@@ -128,13 +128,20 @@ class TestVersionPattern:
         assert p.pattern == pattern
 
     @pytest.mark.parametrize(
-        "content, hits", [("", set()), ("ab12", {"12"}), ("ab12 cd34", {"12", "34"}),]
+        "pattern, content, hits",
+        [
+            (r"(\d+)", "", set()),
+            (r"(\d+)", "ab12", {"12"}),
+            (r"(\d+)", "ab12 cd34", {"12", "34"}),
+            (r"version = (\d+)", "version = 12\nnotversion = 34\nversion = 56not", {"12", "34", "56"}),
+            (r"^version = (\d+)$", "version = 12\nnotversion = 34\nversion = 56not", {"12"}),
+        ],
     )
-    def test_parse(self, tmp_path, content, hits):
+    def test_parse(self, tmp_path, pattern, content, hits):
         path = tmp_path / "pyproject.toml"
         path.write_text(content)
 
-        pattern = VersionPattern(str(path), r"(\d+)")
+        pattern = VersionPattern(str(path), pattern)
         assert pattern.parse() == hits
 
     @pytest.mark.parametrize(