Skip to content

Commit ef4e7f4

Browse files
authored
Centralize plugin version metadata (#15)
* Add pyproject version metadata and sync tooling * Improve plugin version resolution
1 parent 2e1a40a commit ef4e7f4

File tree

4 files changed

+196
-2
lines changed

4 files changed

+196
-2
lines changed

geovita_processing_plugin/__init__.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,73 @@
2626
__date__ = '2024-01-17'
2727
__copyright__ = '(C) 2024 by DPE'
2828

29-
__version__ = "3.2.2"
30-
3129
import sys
30+
import configparser
3231
from pathlib import Path
32+
from importlib import metadata
33+
34+
35+
PACKAGE_NAME = 'geovita-processing-plugin'
36+
_FALLBACK_VERSION = '0.0.0'
37+
_PLUGIN_DIR = Path(__file__).resolve().parent
38+
_PYPROJECT_CANDIDATES = (
39+
_PLUGIN_DIR / 'pyproject.toml',
40+
_PLUGIN_DIR.parent / 'pyproject.toml',
41+
)
42+
_METADATA_FILE = _PLUGIN_DIR / 'metadata.txt'
43+
44+
45+
def _read_version_from_pyproject() -> str | None:
46+
"""Return the version declared in pyproject.toml if available."""
47+
48+
try:
49+
import tomllib # type: ignore[attr-defined]
50+
except ModuleNotFoundError: # pragma: no cover - only on Python <3.11 without tomli
51+
try:
52+
import tomli as tomllib # type: ignore[no-redef]
53+
except ModuleNotFoundError:
54+
return None
55+
56+
for candidate in _PYPROJECT_CANDIDATES:
57+
if not candidate.is_file():
58+
continue
59+
with candidate.open('rb') as fh:
60+
data = tomllib.load(fh)
61+
return data.get('project', {}).get('version')
62+
return None
63+
64+
65+
def _read_version_from_metadata() -> str | None:
66+
"""Return the version declared in metadata.txt if available."""
67+
68+
if not _METADATA_FILE.is_file():
69+
return None
70+
71+
parser = configparser.ConfigParser()
72+
parser.optionxform = str
73+
parser.read(_METADATA_FILE, encoding='utf-8')
74+
try:
75+
return parser.get('general', 'version')
76+
except (configparser.NoSectionError, configparser.NoOptionError):
77+
return None
78+
79+
80+
def _determine_version() -> str:
81+
"""Resolve the plugin version from packaging metadata or local files."""
82+
83+
try:
84+
return metadata.version(PACKAGE_NAME)
85+
except metadata.PackageNotFoundError: # type: ignore[attr-defined]
86+
pass
87+
88+
for reader in (_read_version_from_pyproject, _read_version_from_metadata):
89+
version = reader()
90+
if version:
91+
return version
92+
return _FALLBACK_VERSION
93+
94+
95+
__version__ = _determine_version()
3396

3497

3598
# noinspection PyPep8Naming
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Tests ensuring version consistency across metadata sources."""
2+
from __future__ import annotations
3+
4+
import configparser
5+
from pathlib import Path
6+
7+
import geovita_processing_plugin as plugin
8+
9+
try:
10+
import tomllib # type: ignore[attr-defined]
11+
except ModuleNotFoundError: # pragma: no cover
12+
import tomli as tomllib # type: ignore[no-redef]
13+
14+
15+
REPO_ROOT = Path(__file__).resolve().parents[2]
16+
PYPROJECT_FILE = REPO_ROOT / "pyproject.toml"
17+
METADATA_FILE = REPO_ROOT / "geovita_processing_plugin" / "metadata.txt"
18+
19+
20+
def read_version_from_pyproject() -> str:
21+
with PYPROJECT_FILE.open("rb") as fh:
22+
data = tomllib.load(fh)
23+
return data["project"]["version"]
24+
25+
26+
def read_version_from_metadata() -> str:
27+
parser = configparser.ConfigParser()
28+
parser.optionxform = str
29+
parser.read(METADATA_FILE)
30+
return parser.get("general", "version")
31+
32+
33+
def test_package_version_matches_pyproject():
34+
assert plugin.__version__ == read_version_from_pyproject()
35+
36+
37+
def test_metadata_version_matches_pyproject():
38+
assert read_version_from_metadata() == read_version_from_pyproject()
39+
40+
41+
def test_package_version_matches_metadata():
42+
assert plugin.__version__ == read_version_from_metadata()

pyproject.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[build-system]
2+
requires = ["setuptools>=61"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "geovita-processing-plugin"
7+
version = "3.2.2"
8+
description = "Geovita GIS Processing provider for QGIS"
9+
readme = "README.md"
10+
requires-python = ">=3.10"
11+
authors = [
12+
{ name = "DPE", email = "[email protected]" }
13+
]
14+
license = { file = "LICENSE" }
15+
classifiers = [
16+
"Programming Language :: Python",
17+
"License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)",
18+
"Framework :: QGIS"
19+
]
20+
dynamic = ["dependencies"]
21+
22+
[tool.geovita]
23+
metadata-file = "geovita_processing_plugin/metadata.txt"

scripts/update_metadata_version.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env python3
2+
"""Synchronise metadata.txt version with pyproject.toml."""
3+
from __future__ import annotations
4+
5+
import re
6+
from pathlib import Path
7+
8+
try:
9+
import tomllib # type: ignore[attr-defined]
10+
except ModuleNotFoundError: # pragma: no cover
11+
import tomli as tomllib # type: ignore[no-redef]
12+
13+
14+
REPO_ROOT = Path(__file__).resolve().parent.parent
15+
PYPROJECT_FILE = REPO_ROOT / "pyproject.toml"
16+
METADATA_FILE = REPO_ROOT / "geovita_processing_plugin" / "metadata.txt"
17+
VERSION_PATTERN = re.compile(r"^(version\s*=\s*)(.+)$", re.MULTILINE)
18+
19+
20+
class MetadataVersionError(RuntimeError):
21+
"""Raised when metadata synchronisation cannot be completed."""
22+
23+
24+
def read_version_from_pyproject() -> str:
25+
if not PYPROJECT_FILE.is_file():
26+
raise MetadataVersionError(f"Missing pyproject: {PYPROJECT_FILE}")
27+
28+
with PYPROJECT_FILE.open("rb") as fh:
29+
data = tomllib.load(fh)
30+
31+
project = data.get("project")
32+
if not project or "version" not in project:
33+
raise MetadataVersionError("[project] table missing version")
34+
35+
return project["version"]
36+
37+
38+
def update_metadata_version(new_version: str) -> None:
39+
if not METADATA_FILE.is_file():
40+
raise MetadataVersionError(f"Missing metadata file: {METADATA_FILE}")
41+
42+
contents = METADATA_FILE.read_text(encoding="utf-8")
43+
if "[general]" not in contents:
44+
raise MetadataVersionError("metadata.txt lacks [general] section")
45+
46+
if "version=" not in contents:
47+
raise MetadataVersionError("metadata.txt lacks version entry")
48+
49+
def _replace(match: re.Match[str]) -> str:
50+
return f"{match.group(1)}{new_version}"
51+
52+
updated, count = VERSION_PATTERN.subn(_replace, contents, count=1)
53+
if count != 1:
54+
raise MetadataVersionError("Unexpected number of version entries updated")
55+
56+
METADATA_FILE.write_text(updated, encoding="utf-8")
57+
58+
59+
def main() -> None:
60+
version = read_version_from_pyproject()
61+
update_metadata_version(version)
62+
print(f"Updated metadata version to {version}")
63+
64+
65+
if __name__ == "__main__":
66+
main()

0 commit comments

Comments
 (0)