Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
akx committed Sep 16, 2024
0 parents commit 9a11b5e
Show file tree
Hide file tree
Showing 11 changed files with 435 additions and 0 deletions.
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI

on:
push:
branches:
- master
pull_request:

jobs:
Test:
runs-on: "${{ matrix.os }}"
strategy:
matrix:
os:
- ubuntu-latest
python-version:
- "3.9"
- "3.x"
steps:
- uses: actions/checkout@v4
- name: "Set up Python ${{ matrix.python-version }}"
uses: actions/setup-python@v5
with:
python-version: "${{ matrix.python-version }}"
cache: pip
- run: "pip install -e .[dev]"
- run: py.test -vvv --cov .

Lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pre-commit/action@v3.0.1

Build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -U build
- run: python -m build .
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.py[cod]
.coverage
/htmlcov
build
dist
21 changes: 21 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.5
hooks:
- id: ruff
args:
- --fix
- id: ruff-format

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: debug-statements
- id: end-of-file-fixer
- id: trailing-whitespace
exclude: .*ambr

- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
hooks:
- id: prettier
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2024 Aarni Koskela <akx@iki.fi>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# hatch-calver

A plugin for [hatch][hatch] to support [calendar versioning][calver].

## Setup

Add `hatch-calver` as a build dependency to your project.

```toml
[build-system]
requires = [
"hatchling",
"hatch-calver",
]
build-backend = "hatchling.build"
```

Then, set `tool.hatch.version.scheme` to `"calver"`.

```toml
[tool.hatch.version]
scheme = "calver"
```

### Configuring the CalVer scheme

You can optionally set `calver-scheme` to a dot-separated string
of parts specified in the [calver scheme][calver_scheme] specification.
It defaults to `YYYY.MM.DD`.

```toml
[tool.hatch.version]
scheme = "calver"
calver-scheme = "YY.MM"
```

Note that your project's versions should conform to the scheme you specify;
otherwise, determining where to put e.g. patch versions will be quite ambiguous.

## Usage

You can use Hatch's [standard versioning][hatch_version_updating] commands.

To update your project's version to the current date, run `hatch version release`
(or `hatch version date`).

As with the regular versioning scheme, you can chain multiple segment updates.
The date part of the version will _not_ be updated unless you explicitly specify it.

The CalVer scheme specified for your project specifies which segment of the
PEP 440 "release" segments are automatically determined; for instance, for a `YYYY.MM.DD`
scheme, the 4th field of the release segment will be considered the `patch` field.

In other words, if you specify `YYYY.MM.DD` as your scheme, and it's the 16th of September 2024:

| Original version | Command | New version |
| ---------------- | ----------------------- | --------------- |
| `2024.07.22` | `hatch version release` | `2024.09.16` |
| `2024.07.22` | `hatch version date,a` | `2024.09.16a0` |
| `2021.01.01` | `hatch version rc` | `2021.01.01rc0` |
| `2024.7.22` | `hatch version patch` | `2024.07.22.1` |

[hatch]: https://hatch.pypa.io/
[hatch_version_updating]: https://hatch.pypa.io/latest/version/#updating
[hatch_version_segments]: https://hatch.pypa.io/latest/version/#supported-segments
[calver]: https://calver.org/
[calver_scheme]: https://calver.org/#scheme
68 changes: 68 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "hatch-calver"
description = "Hatch plugin for CalVer versioning"
readme = "README.md"
license = "MIT"
requires-python = ">=3.9"
dependencies = ["hatchling"]
dynamic = ["version"]

authors = [
{ name = "Aarni Koskela", email = "akx@iki.fi" },
]

classifiers = [
"Development Status :: 4 - Beta",
"Framework :: Hatch",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
"Operating System :: OS Independent",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3.9",
"Topic :: Software Development",
]

[project.urls]
Repository = "https://github.com/akx/hatch-calver"
Issues = "https://github.com/akx/hatch-calver/issues"
PyPI = "https://pypi.org/project/hatch-calver/"

[project.entry-points.hatch]
calver = "hatch_calver.hatch_hooks"

[project.optional-dependencies]
dev = ["pytest", "pytest-cov"]

[tool.hatch.version]
path = "src/hatch_calver/__init__.py"
scheme = "calver"
calver-scheme = "YYYY.MM.DD"

[tool.coverage.report]
exclude_lines = [
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]

[tool.ruff]
line-length = 110

[tool.ruff.lint]
select = [
"B",
"COM812",
"E",
"F",
"I",
"W",
]
ignore = [
"E501",
]
1 change: 1 addition & 0 deletions src/hatch_calver/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "2024.9.16rc0"
119 changes: 119 additions & 0 deletions src/hatch_calver/bump.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
Parts of this code has been adapted from the Hatchling project's
`hatchling.version.scheme.standard` module, which is licensed under
the MIT License (Copyright (c) 2017-present Ofek Lev <oss@ofek.dev>).
"""

from __future__ import annotations

import datetime
from typing import Any

from packaging.version import Version

SCHEME_PART_TO_FORMATTER = {
# See https://calver.org/#scheme
"YYYY": lambda d: d.year,
"YY": lambda d: d.year - 2000,
"0Y": lambda d: f"{d.year % 100:02}",
"MM": lambda d: d.month,
"0M": lambda d: f"{d.month:02}",
"WW": lambda d: d.isocalendar()[1],
"0W": lambda d: f"{d.isocalendar()[1]:02}",
"DD": lambda d: d.day,
"0D": lambda d: f"{d.day:02}",
}

SCHEME_PART_MIN_LENGTHS = {
"YYYY": 4,
"YY": 2,
"0D": 2,
"0M": 2,
"0W": 2,
"0Y": 2,
}


def _map_scheme_part(part: str, dt: datetime.datetime) -> str:
formatter = SCHEME_PART_TO_FORMATTER.get(part)
if formatter is None:
err = f"Unknown calver-scheme part: {part} (expected one of {', '.join(SCHEME_PART_TO_FORMATTER)})"
raise ValueError(err)

return str(formatter(dt))


def _update_version(version: Version, **kwargs: Any) -> None:
parts = {}
for part_name in ("epoch", "release", "pre", "post", "dev", "local"):
if part_name in kwargs:
parts[part_name] = kwargs[part_name]
elif parts: # We've set a part, so clear out the following ones
parts[part_name] = None

version._version = version._version._replace(**parts)


def bump_calver(
original_version: str,
desired_version: str,
*,
calver_scheme_string: str = "YYYY.MM.DD",
version_date: datetime.datetime | None = None,
) -> str:
from packaging.version import Version
from packaging.version import _parse_letter_version as parse_letter_version

if not version_date:
version_date = datetime.datetime.now(tz=datetime.timezone.utc)

scheme_parts = str(calver_scheme_string).split(".")
v = Version(original_version)
instructions = desired_version.split(",")
for inst in instructions:
if inst in {"date", "release"}:
# Update the prefix of the current `release`,
# but keep the remaining (non-specified) parts as-is.
release = (
*(_map_scheme_part(part, version_date) for part in scheme_parts),
*v.release[len(scheme_parts) :],
)
v._version = v._version._replace(release=release)
_update_version(v, release=release)
elif inst in {"micro", "patch", "fix"}:
# We'll assume the first part after any part specified by the calver scheme is the micro/patch/fix part.
old_micro = v.release[len(scheme_parts)] if len(v.release) > len(scheme_parts) else 0
new_release = (*v.release[: len(scheme_parts)], old_micro + 1)
_update_version(v, release=new_release)
elif inst in {"a", "b", "c", "rc", "alpha", "beta", "pre", "preview"}:
phase, number = parse_letter_version(inst, 0)
if v.pre:
current_phase, current_number = parse_letter_version(*v.pre)
if phase == current_phase:
number = current_number + 1

_update_version(v, pre=(phase, number))
elif inst in {"post", "rev", "r"}:
number = 0 if v.post is None else v.post + 1
_update_version(v, post=parse_letter_version(inst, number))
elif inst == "dev":
number = 0 if v.dev is None else v.dev + 1
_update_version(v, dev=(inst, number))
else:
if len(instructions) > 1:
raise ValueError("Cannot specify multiple update operations with an explicit version")
return str(inst)
# Small hack – `packaging.Version` strips leading zeroes from the release part, so
# check if we need to pad any of them. This technically breaks the type annotations of
# `packaging._Version`, but `Version.__str__()` doesn't mind...
v._version = v._version._replace(
release=(
tuple(
str(val).zfill(SCHEME_PART_MIN_LENGTHS.get(scheme_parts[i], 1))
if i < len(scheme_parts)
else val
for (i, val) in enumerate(v._version.release)
)
),
)
return str(v)
29 changes: 29 additions & 0 deletions src/hatch_calver/calver_scheme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from __future__ import annotations

from hatchling.version.scheme.plugin.interface import VersionSchemeInterface

from hatch_calver.bump import bump_calver


class CalverScheme(VersionSchemeInterface):
PLUGIN_NAME = "calver"

def update(self, desired_version: str, original_version: str, version_data: dict) -> str:
if not desired_version:
return original_version

version = bump_calver(
original_version,
desired_version,
calver_scheme_string=self.config.get("calver-scheme", "YYYY.MM.DD"),
)

if self.config.get("validate-bump", True):
from packaging.version import Version

next_version = Version(version)
if next_version <= Version(original_version):
raise ValueError(
f"Version `{version}` is not higher than the original version `{original_version}`",
)
return version
8 changes: 8 additions & 0 deletions src/hatch_calver/hatch_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from hatchling.plugin import hookimpl


@hookimpl
def hatch_register_version_scheme():
from hatch_calver.calver_scheme import CalverScheme

return CalverScheme
Loading

0 comments on commit 9a11b5e

Please sign in to comment.