diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 61fc7fc..052a422 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -5,9 +5,9 @@ name: Python package on: push: - branches: [ "main" ] + branches: [ "main", "dev" ] pull_request: - branches: [ "main" ] + branches: [ "main", "dev" ] jobs: build: @@ -16,18 +16,28 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install hatch - - name: Test with pytest - run: | - hatch run test + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install hatch + + - name: Test with pytest + run: hatch run test + + - name: Check code style with hatch + run: hatch fmt --check + + - name: Check typing with mypy + run: | + hatch run types:check + hatch run types:check-tests diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..004c84f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +Antares Study Version Changelog +=============================== + +v0.1.1 (2024-04-10) +------------------- + +### Fixes + +* **model:** avoid deprecation warning about usage of `NotImplemented` in boolean context + +### Tests + +* replace [pytest-freezegun](https://pypi.org/project/pytest-freezegun/) lib + by [pytest-freezer](https://pypi.org/project/pytest-freezer/) + +### Docs + +* [README.md](README.md): update doc, fix typo, add a link to the change log + +### Build + +* create [CHANGELOG.md](CHANGELOG.md) and update the release date +* add [update_version.py](scripts/update_version.py) script to create or update the changelog + +### CI + +* add missing dependency [pytest-freezer](https://pypi.org/project/pytest-freezer/) required for type checking +* create the [GitHub workflows](.github/workflows/python-package.yml) for code style and typing check + +v0.1.0 (2024-03-20) +------------------- + +### Features + +* First release of the project. + diff --git a/README.md b/README.md index a467ec0..c296eab 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,10 @@ - [SolverVersion](#solverversion) - [Pydantic model](#pydantic-model) - [Development](#development) + - [Development tasks](#development-tasks) + - [Building the package](#building-the-package) - [License](#license) +- [Changelog](CHANGELOG.md) ## Overview @@ -22,7 +25,7 @@ The `antares-study-version` package defines `StudyVersion` and `SolverVersion` c It can be used to manage the version of a study, but also the version of [Antares Solver](https://github.com/AntaresSimulatorTeam/Antares_Simulator). It supports the [semver](https://semver.org/) format ("major.minor.patch") and the integer format -(major*100 + minor*10 + patch), which is specific to Antares. +(major×100 + minor×10 + patch), which is specific to Antares. This module harmonizes the management of versions in Antares: @@ -31,7 +34,7 @@ This module harmonizes the management of versions in Antares: In the data of a study and in the programs, we encounter several version formats: -- dotted string (ex. "8.7" or "8.7.2"), +- dotted string (ex. `"8.7"` or `"8.7.2"`), - compact string (ex. `"870"`), - integer (ex. `870`). - tuples or lists (ex. `(8, 7)` or `[8, 7, 2]`). @@ -42,8 +45,8 @@ For instance, since of Antares Solver, versions are stored as dotted strings; the compact format is now obsolete (backward compatibility is ensured for versions prior to 9.0); -For instance, the `study.antares` configuration file now uses the "X.Y" format for the study version instead of the " -XYZ" format. +For instance, the `study.antares` configuration file now uses the "X.Y" format for the study version instead +of the "XYZ" format. ```ini [antares] @@ -238,7 +241,7 @@ hatch fmt > See [hatch fmt](https://hatch.pypa.io/latest/cli/reference/#hatch-fmt) documentation -➢ To run the tests, run: +➢ To run the tests on the current Python version, run: ```shell hatch run test @@ -246,6 +249,12 @@ hatch run test > See [hatch run](https://hatch.pypa.io/latest/cli/reference/#hatch-run) documentation +➢ To run the tests on Python 3.12, for example, run: + +```shell +hatch run all.py3.12:test +``` + ➢ To generate the test coverage report, run: ```shell diff --git a/pyproject.toml b/pyproject.toml index 081a945..40d6226 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "coverage[toml]>=6.5", "pytest<8", "pydantic<2", - "pytest-freezegun<0.5", + "pytest-freezer<0.5", ] [tool.hatch.envs.default.scripts] @@ -67,6 +67,7 @@ python = ["3.8", "3.9", "3.10", "3.11", "3.12"] dependencies = [ "pytest<8", "pydantic<2", + "pytest-freezer<0.5", "mypy>=1.0.0", ] [tool.hatch.envs.types.scripts] diff --git a/scripts/update_version.py b/scripts/update_version.py new file mode 100755 index 0000000..6a909db --- /dev/null +++ b/scripts/update_version.py @@ -0,0 +1,245 @@ +#!/usr/bin/python3 +""" +Script used to update the version number and release date of Antares-Study-Version. +""" + +import argparse +import datetime +import pathlib +import re +import typing as t + +try: + from antares.study.__about__ import __version__ +except ImportError: + __version__ = "(unknown): use the virtualenv's Python to get the actual version number." + +HERE = pathlib.Path(__file__).parent.resolve() +PROJECT_DIR = next(iter(p for p in HERE.parents if p.joinpath("src").exists())) + +# ============================================================== +# List of files to patch (path are relative to the project root) +# ============================================================== + +ABOUT_FILE = "src/antares/study/version/__about__.py" +CHANGELOG_FILE = "CHANGELOG.md" +FILES_TO_PATCH = [ + ( + ABOUT_FILE, + "__version__ =.*", + '__version__ = "{new_version}"', + ), +] + +# ============================================================== +# Template of change log file (used when the file doesn't exist) +# ============================================================== + +CHANGELOG_TEMPLATE = """\ +Antares Study Version Changelog +=============================== + +v{new_version} ({new_date}) +-------------------- + +### Features + +* First release of the project. + +""" + +# ================================================ +# Regular expressions used to parse the change log +# ================================================ + +TOKENS = [ + ("H1", r"^([^\n]+)\n={3,}$"), + ("H2", r"^([^\n]+)\n-{3,}$"), + ("H3", r"^#{3}\s+([^\n]+)$"), + ("H4", r"^#{4}\s+([^\n]+)$"), + ("LINE", r"^[^\n]+$"), + ("NEWLINE", r"\n"), + ("MISMATCH", r"."), +] + +ANY_TOKEN_RE = "|".join([f"(?P<{name}>{regex})" for name, regex in TOKENS]) + + +class Token: + def __init__(self, kind: str, text: str) -> None: + self.kind = kind + self.text = text + + def __str__(self) -> str: + return self.text + + +class NewlineToken(Token): + def __init__(self) -> None: + super().__init__("NEWLINE", "\n") + + +class TitleToken(Token): + def __init__(self, kind: str, text: str) -> None: + super().__init__(kind, text) + + @property + def level(self) -> int: + return int(self.kind[1:]) + + def __str__(self) -> str: + title = self.text.strip() + if self.level == 1: + return "\n".join([title, "=" * len(title)]) + elif self.level == 2: + return "\n".join([title, "-" * len(title)]) + else: + return "#" * self.level + " " + title + + +def parse_changelog(change_log: str) -> t.Generator[Token, None, None]: + for mo in re.finditer(ANY_TOKEN_RE, change_log, flags=re.MULTILINE): + kind = mo.lastgroup + if kind in {"H1", "H2", "H3", "H4"} and mo.lastindex is not None: + title = mo[mo.lastindex + 1] + yield TitleToken(kind, title) + elif kind in {"FRONT_MATTER", "LINE"}: + yield Token(kind, mo.group()) + elif kind == "NEWLINE": + yield NewlineToken() + else: + raise NotImplementedError(kind, mo.group()) + + +def update_changelog(change_log: str, new_version: str, new_date: str) -> t.Generator[Token, None, None]: + new_title = f"v{new_version} ({new_date})" + + first_release_found = False + for token in parse_changelog(change_log): + if first_release_found: + yield token + continue + + is_release_title = isinstance(token, TitleToken) and token.level == 2 + if is_release_title: + first_release_found = True + if token.text.split()[0] == new_title.split()[0]: + # Update the release date + yield TitleToken(kind=token.kind, text=new_title) + else: + # Insert a new release title before the current one + yield TitleToken(kind=token.kind, text=new_title) + yield NewlineToken() + yield NewlineToken() + yield NewlineToken() + yield token + + else: + yield token + + +def upgrade_version(new_version: str, new_date: str) -> None: + """ + Update the version number and release date in specific files. + + Args: + new_version: The new version number to update. + new_date: The new release date to update. + + Returns: + + """ + print(f"Updating files to version {new_version}...") + for rel_path, search, replace in FILES_TO_PATCH: + replace = replace.format(new_version=new_version) + fullpath = PROJECT_DIR.joinpath(rel_path) + if fullpath.is_file(): + print(f"- updating '{fullpath.relative_to(PROJECT_DIR)}'...") + text = fullpath.read_text(encoding="utf-8") + patched = re.sub(search, replace, text, count=2) + fullpath.write_text(patched, encoding="utf-8") + + # Patching release date + search = "__date__ =.*" + replace = f'__date__ = "{new_date}"' + + print(f"Updating the release date to {new_date}...") + fullpath = PROJECT_DIR.joinpath(ABOUT_FILE) + text = fullpath.read_text(encoding="utf-8") + patched = re.sub(search, replace, text, count=1) + fullpath.write_text(patched, encoding="utf-8") + + print("Updating the CHANGELOG...") + changelog_path = PROJECT_DIR.joinpath(CHANGELOG_FILE) + if not changelog_path.is_file(): + with changelog_path.open(mode="w", encoding="utf-8") as fd: + changelog = CHANGELOG_TEMPLATE.format(new_version=new_version, new_date=new_date) + print(changelog, end="", file=fd) + else: + change_log = changelog_path.read_text(encoding="utf-8") + with changelog_path.open(mode="w", encoding="utf-8") as fd: + for token in update_changelog(change_log, new_version, new_date): + print(token, end="", file=fd) + + print("The version has been successfully updated.") + + +class RegexType: + """ + Type of un argument which is checked by a regex. + """ + + def __init__(self, regex: str) -> None: + self.regex = re.compile(regex) + + def __call__(self, string: str) -> str: + if self.regex.fullmatch(string): + return string + pattern = self.regex.pattern + msg = f"Invalid value '{string}': it doesn't match '{pattern}'" + raise argparse.ArgumentTypeError(msg) + + +DESCRIPTION = """\ +Upgrade the version number and release date of Antares-Study-Version. + +Use this script to update the version number and release date of the Antares-Study-Version application. +It is designed to be executed before releasing the application on GitHub, specifically +when a new version is completed in the `release` or `hotfix` branch. +""" + + +def main() -> None: + parser = argparse.ArgumentParser(prog="upgrade_version", description=DESCRIPTION) + parser.add_argument( + "--version", + dest="new_version", + action="version", + version=__version__, + help="show the current version and exit", + ) + date_type = RegexType(regex=r"\d{4}-\d{2}-\d{2}") + parser.add_argument( + "-d", + "--date", + dest="new_date", + type=date_type, + default=datetime.date.today().isoformat(), + help=f"new release date, using the format '{date_type.regex.pattern}'", + metavar="ISO_DATE", + ) + version_type = RegexType(regex=r"v?\d+(?:\.\d+)+") + parser.add_argument( + "new_version", + type=version_type, + help=f"new application version, using the format '{version_type.regex.pattern}'", + metavar="VERSION", + ) + + args = parser.parse_args() + args.new_version = args.new_version.lstrip("v") + upgrade_version(args.new_version, args.new_date) + + +if __name__ == "__main__": + main() diff --git a/src/antares/study/version/__about__.py b/src/antares/study/version/__about__.py index 3094fe3..8fc2726 100644 --- a/src/antares/study/version/__about__.py +++ b/src/antares/study/version/__about__.py @@ -4,7 +4,7 @@ # Standard project metadata -__version__ = "0.1.0" +__version__ = "0.1.1" __author__ = "RTE, Antares Web Team" -__date__ = "unreleased" +__date__ = "2024-04-10" __credits__ = "© Réseau de Transport de l’Électricité (RTE)" diff --git a/src/antares/study/version/model.py b/src/antares/study/version/model.py index 58b5f5a..e332bc3 100644 --- a/src/antares/study/version/model.py +++ b/src/antares/study/version/model.py @@ -60,7 +60,12 @@ def __ne__(self, other: object) -> bool: return NotImplemented def __eq__(self, other: object) -> bool: - return not self.__ne__(other) + if isinstance(other, _TripletVersion): + return (self.major, self.minor, self.patch).__eq__((other.major, other.minor, other.patch)) + elif isinstance(other, (int, str, t.Sequence, t.Mapping)): + return self.__eq__(self.parse(other)) + else: + return NotImplemented def __lt__(self, other): if isinstance(other, _TripletVersion):