Skip to content

Commit

Permalink
Merge pull request #7 from brainelectronics/feature/convert-script-to…
Browse files Browse the repository at this point in the history
…-class

Convert version extraction script to class
  • Loading branch information
brainelectronics authored Aug 3, 2022
2 parents 679d5e8 + 1370586 commit f60b542
Show file tree
Hide file tree
Showing 9 changed files with 455 additions and 182 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ changelog2version \
--debug
```

## Advanced

### Custom regular expressions
To extract a version line from a given changelog file with an alternative
regex, the `version_line_regex` argument can be used as shown below. The
expression is validated during the CLI argument parsing

```bash
changelog2version \
--changelog_file changelog.md \
--version_file src/changelog2version/version.py \
--version_line_regex "^\#\# \[\d{1,}[.]\d{1,}[.]\d{1,}\]" \
--debug
```

Same applies for a custom semver line regex in order to extract the semantic
version part from a full version line, use the `semver_line_regex` argument to
adjust the regular expression to your needs.

## Credits

Based on the [PyPa sample project][ref-pypa-sample].
Expand Down
26 changes: 22 additions & 4 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ r"^\#\# \[\d{1,}[.]\d{1,}[.]\d{1,}\] \- \d{4}\-\d{2}-\d{2}$"
-->

## Released
## [0.2.0] - 2022-08-03
### Added
- [`ExtractVersion class`](src/changelog2version/extract_version.py) to
extract the version line from a changelog file and to parse the semver
content from a version line, see [#4][ref-issue-4]
- `semver_line_regex` and `version_line_regex` args for `changelog2version` to
provide custom regular expressions to parse a version line from a changelog
and to extract the semver content from a line

### Changed
- Main parsing code of
[`update_version script`](src/changelog2version/update_version.py) moved to
new [`ExtractVersion class`](src/changelog2version/extract_version.py)
- Extend usage example in [`README`](README.md) file
- Rename [test data changelog files](tests/data/valid)
- Split unittest for `ExtractVersion` from `update_version` test
- Let the pipeline fail is there are flake8 violations

## [0.1.1] - 2022-07-31
### Fixed
- Update root [`README`](README.md) file with usage instructions
Expand Down Expand Up @@ -53,12 +71,12 @@ r"^\#\# \[\d{1,}[.]\d{1,}[.]\d{1,}\] \- \d{4}\-\d{2}-\d{2}$"
- Data folder after fork

<!-- Links -->
[Unreleased]: https://github.com/brainelectronics/changelog2version/compare/0.1.1...develop
[Unreleased]: https://github.com/brainelectronics/changelog2version/compare/0.2.0...develop

[0.2.0]: https://github.com/brainelectronics/changelog2version/tree/0.2.0
[0.1.1]: https://github.com/brainelectronics/changelog2version/tree/0.1.1
[0.1.0]: https://github.com/brainelectronics/changelog2version/tree/0.1.0

<!--
[ref-issue-1]: https://github.com/brainelectronics/changelog2version/issues/1
-->
[ref-issue-4]: https://github.com/brainelectronics/changelog2version/issues/4

[ref-python-gitignore-template]: https://github.com/github/gitignore/blob/e5323759e387ba347a9d50f8b0ddd16502eb71d4/Python.gitignore
180 changes: 180 additions & 0 deletions src/changelog2version/extract_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

"""
Extract a single version line from a changelog and extract the semver content
from this line
"""

import logging
from pathlib import Path
import re
import semver
from sys import stdout
from typing import Optional


class ExtractVersionError(Exception):
"""Base class for exceptions in this module."""
pass


class ExtractVersion(object):
"""Extract the version line and SemVer part from a changelog file"""
def __init__(self,
# version_line_regex: Optional[str] = None,
# semver_line_regex: Optional[str] = None,
logger: Optional[logging.Logger] = None):
"""
Init ExtractVersion class
:param version_line_regex: Regex for the complete version line
:type version_line_regex: Optional[str]
:param semver_line_regex: Regex for the semver part of the
complete version line
:type semver_line_regex: Optional[str]
:param logger: Logger object
:type logger: Optional[logging.Logger]
"""
if logger is None:
logger = self._create_logger()
self._logger = logger

# append "$" to match only ISO8601 dates without additional timestamps
self._version_line_regex = r"^\#\# \[\d{1,}[.]\d{1,}[.]\d{1,}\] \- \d{4}\-\d{2}-\d{2}" # noqa
self._semver_line_regex = r"\[\d{1,}[.]\d{1,}[.]\d{1,}\]"

@property
def version_line_regex(self) -> str:
"""
Get regex to extract complete version line from changelog
:returns: Regex of the complete version line
:rtype: str
"""
return self._version_line_regex

@version_line_regex.setter
def version_line_regex(self, value: str) -> None:
"""
Set regex to extract complete version line from changelog
:param value: Regex to get the complete version line
:type value: str
"""
try:
re.compile(value)
self._version_line_regex = value
except re.error:
raise ExtractVersionError("Invalid regex pattern")

@property
def semver_line_regex(self) -> str:
"""
Get regex to extract the semver part from the complete version line
:returns: Regex of the semver part
:rtype: str
"""
return self._semver_line_regex

@semver_line_regex.setter
def semver_line_regex(self, value: str) -> None:
"""
Set regex to extract the semver part from the complete version line
:param value: Regex to get the semver part
:type value: str
"""
try:
re.compile(value)
self._semver_line_regex = value
except re.error:
raise ExtractVersionError("Invalid regex pattern")

def _create_logger(self, logger_name: str = None) -> logging.Logger:
"""
Create a logger
:param logger_name: The logger name
:type logger_name: str, optional
:returns: Configured logger
:rtype: logging.Logger
"""
custom_format = '[%(asctime)s] [%(levelname)-8s] [%(filename)-15s @'\
' %(funcName)-15s:%(lineno)4s] %(message)s'

# configure logging
logging.basicConfig(level=logging.INFO,
format=custom_format,
stream=stdout)

if logger_name and (isinstance(logger_name, str)):
logger = logging.getLogger(logger_name)
else:
logger = logging.getLogger(__name__)

# set the logger level to DEBUG if specified differently
logger.setLevel(logging.DEBUG)

return logger

def parse_changelog(self, changelog_file: Path) -> str:
"""
Parse the changelog for the first matching version line
:param changelog_file: The path to the changelog file
:type changelog_file: Path
:returns: Extracted semantic version string
:rtype: str
"""
release_version_line = ""

with open(changelog_file, "r") as f:
for line in f:
match = re.search(self.version_line_regex, line)
if match:
release_version_line = match.group()
break

self._logger.debug("First matching release version line: '{}'".
format(release_version_line))

return release_version_line

def parse_semver_line(self, release_version_line: str) -> str:
"""
Parse a version line for a semantic version
Examples of a valid SemVer line:
- "## [0.2.0] - 2022-05-19"
- "## [107.3.18] - 1900-01-01 12:34:56"
:param release_version_line: The release version line
:type release_version_line: str
:returns: Semantic version string, e.g. "0.2.0"
:rtype: str
"""
semver_string = "0.0.0"

# extract semver from release version line
match = re.search(self.semver_line_regex, release_version_line)
if match:
semver_string = match.group()
# remove '[' and ']' from semver_string
semver_string = re.sub(r"[\[\]]", "", semver_string)
if not semver.VersionInfo.isvalid(semver_string):
self._logger.error("Parsed SemVer string is invalid, check "
"the changelog format")
raise ValueError("Invalid SemVer string")
self._logger.debug("Extracted SemVer string: '{}'".
format(semver_string))
else:
self._logger.warning("No SemVer string found in given release "
"version line: '{}'".
format(release_version_line))

return semver_string
114 changes: 50 additions & 64 deletions src/changelog2version/update_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import semver
from sys import stdout

from .extract_version import ExtractVersion


def parser_valid_file(parser: argparse.ArgumentParser, arg: str) -> str:
"""
Expand All @@ -45,6 +47,24 @@ def parser_valid_file(parser: argparse.ArgumentParser, arg: str) -> str:
return arg


def validate_regex(parser: argparse.ArgumentParser, arg: str) -> str:
"""
Validate given regex pattern
:param parser: The parser
:type parser: parser object
:param arg: The regex pattern to check
:type arg: str
:raise argparse.ArgumentError: Argument is not a file
:returns: Regex pattern, parser error is thrown otherwise.
:rtype: str
"""
try:
re.compile(arg)
except re.error:
parser.error("The regex pattern '{}' is invalid".format(arg))
return arg


def parse_arguments() -> argparse.Namespace:
"""
Parse CLI arguments.
Expand Down Expand Up @@ -79,72 +99,23 @@ def parse_arguments() -> argparse.Namespace:
choices=['py'],
help='Type of version file to generate')

parsed_args = parser.parse_args()

return parsed_args


def parse_changelog(changelog_file: Path, logger: logging.Logger) -> str:
"""
Parse the changelog for the first matching release version line
:param changelog_file: The path to the changelog file
:type changelog_file: Path
:param logger: Logger object
:type logger: logging.Logger
:returns: Extracted semantic version string
:rtype: str
"""
release_version_line_regex = r"^\#\# \[\d{1,}[.]\d{1,}[.]\d{1,}\] \- \d{4}\-\d{2}-\d{2}" # noqa
# append "$" to match only ISO8601 dates without additional timestamps
release_version_line = ""

with open(changelog_file, "r") as f:
for line in f:
match = re.search(release_version_line_regex, line)
if match:
release_version_line = match.group()
break

logger.debug("First matching release version line: '{}'".
format(release_version_line))

return parse_semver_line(release_version_line, logger)


def parse_semver_line(release_version_line: str,
logger: logging.Logger) -> str:
"""
Parse a release version line for a semantic version
parser.add_argument('--version_line_regex',
dest='version_line_regex',
required=False,
type=lambda x: validate_regex(parser, x),
help='Regex to extract complete version line from a '
'changelog')

:param release_version_line: The release version line as described
:type release_version_line: str
:param logger: Logger object
:type logger: logging.Logger
parser.add_argument('--semver_line_regex',
dest='semver_line_regex',
required=False,
type=lambda x: validate_regex(parser, x),
help='Regex to extract semver part of from a version '
'line')

:returns: Semantic version string
:rtype: str
"""
# release_version_line = "## [0.2.0] - 2022-05-19"
semver_regex = r"\[\d{1,}[.]\d{1,}[.]\d{1,}\]"
semver_string = "0.0.0"

# extract semver from release version line
match = re.search(semver_regex, release_version_line)
if match:
semver_string = match.group()
# remove '[' and ']' from semver_string
semver_string = re.sub(r"[\[\]]", "", semver_string)
if not semver.VersionInfo.isvalid(semver_string):
logger.error("Parsed SemVer string is invalid, check format")
raise ValueError("Invalid SemVer string")
logger.debug("Extracted SemVer string: '{}'".format(semver_string))
else:
logger.warning("No SemVer string found in given release version line")
parsed_args = parser.parse_args()

# semver_string = "0.2.0"
return semver_string
return parsed_args


def create_version_info_line(semver_string: str,
Expand Down Expand Up @@ -211,11 +182,26 @@ def main():

changelog_file = Path(args.changelog_file).resolve()
version_file = Path(args.version_file).resolve()
version_line_regex = args.version_line_regex
semver_line_regex = args.semver_line_regex

logger.debug("Using changelog file '{}' to update version file '{}'".
format(changelog_file, version_file))

semver_string = parse_changelog(changelog_file, logger)
version_extractor = ExtractVersion(logger=logger)

if semver_line_regex:
logger.debug("Use this regex to get the semver part from the "
"version line: {}".format(semver_line_regex))
version_extractor.semver_line_regex = semver_line_regex

if version_line_regex:
logger.debug("Use this regex to get the version line from the "
"changelog file: {}".format(version_line_regex))
version_extractor.version_line_regex = version_line_regex

version_line = version_extractor.parse_changelog(changelog_file)
semver_string = version_extractor.parse_semver_line(version_line)
version_info_line = create_version_info_line(semver_string, logger)
update_version_file(version_file, version_info_line, logger)

Expand Down
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit f60b542

Please sign in to comment.