diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index cf77226..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,27 +0,0 @@ -[bumpversion] -current_version = 0.5.0 -commit = True -tag = True -tag_name = v{new_version} -tag_message = Release version {new_version} -message = Bump version: {current_version} → {new_version} - -[bumpversion:file:pyproject.toml] -search = version = "{current_version}" -replace = version = "{new_version}" - -[bumpversion:file:src/alpha_hwr/__init__.py] -search = __version__ = "{current_version}" -replace = __version__ = "{new_version}" - -[bumpversion:file:CHANGELOG.md] -search = ## [Unreleased] -replace = ## [Unreleased] - -[bumpversion:file:mkdocs.yml] -search = version: "{current_version}" -replace = version: "{new_version}" - -[bumpversion:file:docs/index.md] -search = *Version: {current_version}* -replace = *Version: {new_version}* diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b20b3fd..42ec2cf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,7 +2,8 @@ name: Publish to PyPI on: release: - types: [published] + types: + - published jobs: build-and-publish: @@ -24,18 +25,8 @@ jobs: - name: Install build dependencies run: | python -m pip install --upgrade pip - pip install build twine - - - name: Verify version matches tag - run: | - TAG_VERSION=${GITHUB_REF#refs/tags/v} - PACKAGE_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])") - if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then - echo "Error: Tag version ($TAG_VERSION) does not match package version ($PACKAGE_VERSION)" - exit 1 - fi - echo "Version check passed: $PACKAGE_VERSION" - + pip install build twine setuptools_scm + - name: Build package run: python -m build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ec7278..e939a6b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -197,9 +197,8 @@ All new features should include appropriate tests. Releases are managed by maintainers: 1. Update CHANGELOG.md -2. Run `bump2version [major|minor|patch]` to create version tag -3. Push tags: `git push --tags` -4. Create a GitHub Release - this triggers PyPI publishing automatically +2. Push a version tag: `git tag v && git push --tags` +3. Create a GitHub Release from that tag - this triggers PyPI publishing automatically ## Questions? diff --git a/pyproject.toml b/pyproject.toml index aab765f..1df6b34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=61.0", "wheel"] +requires = ["setuptools>=61.0", "setuptools_scm>=8.0", "wheel"] build-backend = "setuptools.build_meta" [tool.uv] @@ -13,13 +13,12 @@ dev-dependencies = [ "pytest-xdist>=3.0.0", "hypothesis>=6.0.0", "tox>=4.0.0", - "bump2version>=1.0.0", "basedpyright>=1.0.0", ] [project] name = "alpha-hwr" -version = "0.5.0" +dynamic = ["version"] description = "Modern Python library and CLI for Grundfos ALPHA HWR pumps via Bluetooth Low Energy" readme = "README.md" authors = [ @@ -81,7 +80,6 @@ dev = [ "mypy>=1.0.0", "ruff>=0.1.0", "tox>=4.0.0", - "bump2version>=1.0.0", "basedpyright>=1.0.0", ] docs = [ @@ -126,3 +124,7 @@ markers = [ database = false max_examples = 100 suppress_health_check = ["too_slow"] + +[tool.setuptools_scm] +version_scheme = "post-release" +local_scheme = "no-local-version" diff --git a/src/alpha_hwr/__init__.py b/src/alpha_hwr/__init__.py index 297ed30..44aba80 100644 --- a/src/alpha_hwr/__init__.py +++ b/src/alpha_hwr/__init__.py @@ -2,7 +2,12 @@ Grundfos ALPHA HWR Client Library """ -__version__ = "0.5.0" +from importlib.metadata import version, PackageNotFoundError + +try: + __version__ = version("alpha-hwr") +except PackageNotFoundError: + __version__ = "0.0.0+unknown" from .client import AlphaHWRClient, discover_devices from .models import ( diff --git a/tests/test_version_consistency.py b/tests/test_version_consistency.py index 49d1164..057d5c1 100644 --- a/tests/test_version_consistency.py +++ b/tests/test_version_consistency.py @@ -1,139 +1,43 @@ -"""Test version consistency across the project files.""" +"""Test version configuration for the project.""" import re from pathlib import Path -def get_version_from_pyproject(): - """Extract version from pyproject.toml.""" +def test_version_consistency(): + """Test that pyproject.toml is correctly configured for setuptools_scm.""" pyproject_path = Path(__file__).parent.parent / "pyproject.toml" content = pyproject_path.read_text() - match = re.search(r'version = "([^"]+)"', content) - if match: - return match.group(1) - return None - -def get_version_from_init(): - """Extract version from __init__.py.""" - init_path = ( - Path(__file__).parent.parent / "src" / "alpha_hwr" / "__init__.py" + # Version must be dynamic, not static + assert 'dynamic = ["version"]' in content, ( + "pyproject.toml must declare version as dynamic for setuptools_scm" ) - content = init_path.read_text() - match = re.search(r'__version__ = "([^"]+)"', content) - if match: - return match.group(1) - return None - - -def get_version_from_mkdocs(): - """Extract version from mkdocs.yml.""" - mkdocs_path = Path(__file__).parent.parent / "mkdocs.yml" - content = mkdocs_path.read_text() - match = re.search(r'version: "([^"]+)"', content) - if match: - return match.group(1) - return None - - -def get_version_from_docs_index(): - """Extract version from docs/index.md.""" - docs_index_path = Path(__file__).parent.parent / "docs" / "index.md" - content = docs_index_path.read_text() - match = re.search(r"\*Version: ([^\*]+)\*", content) - if match: - return match.group(1) - return None - - -def get_version_from_bumpversion_cfg(): - """Extract current_version from .bumpversion.cfg.""" - bumpversion_path = Path(__file__).parent.parent / ".bumpversion.cfg" - content = bumpversion_path.read_text() - match = re.search(r"current_version = (.+)", content) - if match: - return match.group(1).strip() - return None - - -def test_version_consistency(): - """Test that version is consistent across all project files.""" - pyproject_version = get_version_from_pyproject() - init_version = get_version_from_init() - mkdocs_version = get_version_from_mkdocs() - docs_index_version = get_version_from_docs_index() - bumpversion_version = get_version_from_bumpversion_cfg() + assert ( + re.search(r'^\s*version\s*=\s*"[^"]+"', content, re.MULTILINE) is None + ), "pyproject.toml must not have a static version field in [project]" - # Ensure all versions were found - assert pyproject_version is not None, "Version not found in pyproject.toml" - assert init_version is not None, "Version not found in __init__.py" - assert mkdocs_version is not None, "Version not found in mkdocs.yml" - assert docs_index_version is not None, "Version not found in docs/index.md" - assert bumpversion_version is not None, ( - "Version not found in .bumpversion.cfg" + # setuptools_scm must be in build requirements + assert "setuptools_scm" in content, ( + "setuptools_scm must be listed in [build-system].requires" ) - # Check consistency - assert init_version == pyproject_version, ( - f"Version mismatch: __init__.py ({init_version}) != " - f"pyproject.toml ({pyproject_version})" - ) - assert mkdocs_version == pyproject_version, ( - f"Version mismatch: mkdocs.yml ({mkdocs_version}) != " - f"pyproject.toml ({pyproject_version})" - ) - assert docs_index_version == pyproject_version, ( - f"Version mismatch: docs/index.md ({docs_index_version}) != " - f"pyproject.toml ({pyproject_version})" - ) - assert bumpversion_version == pyproject_version, ( - f"Version mismatch: .bumpversion.cfg ({bumpversion_version}) != " - f"pyproject.toml ({pyproject_version})" + # [tool.setuptools_scm] section must exist + assert "[tool.setuptools_scm]" in content, ( + "pyproject.toml must have a [tool.setuptools_scm] section" ) -def test_bumpversion_search_patterns(): - """Test that bumpversion search patterns match actual file content.""" - pyproject_path = Path(__file__).parent.parent / "pyproject.toml" +def test_init_uses_importlib_metadata(): + """Test that __init__.py reads version from importlib.metadata.""" init_path = ( Path(__file__).parent.parent / "src" / "alpha_hwr" / "__init__.py" ) - mkdocs_path = Path(__file__).parent.parent / "mkdocs.yml" - docs_index_path = Path(__file__).parent.parent / "docs" / "index.md" - bumpversion_path = Path(__file__).parent.parent / ".bumpversion.cfg" - - # Get current version - version = get_version_from_pyproject() - assert version is not None, "Could not determine current version" - - # Check that patterns would match - pyproject_content = pyproject_path.read_text() - assert f'version = "{version}"' in pyproject_content, ( - f'pyproject.toml does not contain expected pattern: version = "{version}"' - ) - - init_content = init_path.read_text() - assert f'__version__ = "{version}"' in init_content, ( - f'__init__.py does not contain expected pattern: __version__ = "{version}"' - ) - - mkdocs_content = mkdocs_path.read_text() - assert f'version: "{version}"' in mkdocs_content, ( - f'mkdocs.yml does not contain expected pattern: version: "{version}"\n' - f"This usually means the indentation or quotes don't match the bumpversion config." - ) + content = init_path.read_text() - docs_index_content = docs_index_path.read_text() - assert f"*Version: {version}*" in docs_index_content, ( - f"docs/index.md does not contain expected pattern: *Version: {version}*" + assert "importlib.metadata" in content, ( + "__init__.py must use importlib.metadata to read the package version" ) - - # Verify .bumpversion.cfg has correct patterns - bumpversion_content = bumpversion_path.read_text() - - # Check mkdocs.yml pattern includes proper indentation - # The pattern should have 2 spaces before 'version:' - assert 'search = version: "{current_version}"' in bumpversion_content, ( - ".bumpversion.cfg mkdocs.yml search pattern must include proper indentation " - "(2 spaces before 'version:')" + assert "PackageNotFoundError" in content, ( + "__init__.py must handle PackageNotFoundError from importlib.metadata" )