Skip to content

Commit 6156168

Browse files
committed
Fixes #10
1 parent a3bc930 commit 6156168

File tree

11 files changed

+110
-78
lines changed

11 files changed

+110
-78
lines changed

.github/workflows/check-same-version.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ jobs:
2222
- name: Install Python
2323
uses: actions/setup-python@v5
2424
with:
25-
python-version: ">=3.12" # required by same-version
25+
python-version: ">=3.10" # required by same-version
2626
cache: 'pip' # optional and only works for Python projects
2727

2828
- name: Run same-version
29-
uses: willynilly/same-version@v4.0.0
29+
uses: willynilly/same-version@v5.0.0
3030
with:
3131
fail_for_missing_file: false
3232
check_github_event: true
@@ -46,6 +46,7 @@ jobs:
4646
check_r_description: false
4747
check_composer_json: false
4848
check_pom_xml: false
49+
check_nuspec: false
4950
check_cargo_toml: false
5051
check_ro_crate_metadata_json: false
5152
check_py_version_assignment: false

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ keywords:
3434
- metadata
3535
- harmonization
3636
license: Apache-2.0
37-
version: "4.0.0"
37+
version: "5.0.0"
3838
date-released: "2025-06-10"
3939
references:
4040
- title: Citation File Format

README.md

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,12 @@ This workflow runs after the tag or release exists and can report problems, but
6464
- `composer.json` (PHP)
6565
- `Cargo.toml` (Rust)
6666
- `pom.xml` (Java)
67+
- `.nuspec` (.NET/C#/NuGet)
6768
- `DESCRIPTION` (R)
6869
- `ro-crate-metadata.json` (RO-Crate)
6970

7071

71-
✅ Cross-language support (e.g., Python, R, JS/TypeScript, Java, Rust, PHP)
72+
✅ Cross-language support (e.g., Python, R, JS/TypeScript, Java, Rust, PHP, C#)
7273

7374
✅ Cross-standard support for FAIR and Open Science metadata (e.g., CFF, CodeMeta, RO-Crate, Zenodo)
7475

@@ -92,22 +93,24 @@ This workflow runs after the tag or release exists and can report problems, but
9293

9394
These files are currently supported out-of-the-box:
9495

95-
| File | Parser used |
96-
|------------------|-------------|
96+
| File | Initial Version Format (translates to PEP 440 for comparison) |
97+
|------------------|-------------------|
9798
| `CITATION.cff` | PEP 440 |
9899
| `pyproject.toml` | PEP 440 |
99100
| `setup.py` | PEP 440 |
100-
| `package.json` | Strict SemVer (converted from canonical PEP 440 tag) |
101+
| `package.json` | Strict SemVer |
101102
| `codemeta.json` | PEP 440 |
102103
| `.zenodo.json` | PEP 440 |
103104
| `composer.json` | PEP 440 |
104105
| `Cargo.toml` | PEP 440 |
105106
| `pom.xml` | PEP 440 |
107+
| `.nuspec` | Strict SemVer |
106108
| R `DESCRIPTION` file | PEP 440 |
107109
| Python file with `__version__` assignment | PEP 440 |
108110
| `ro-crate-metadata.json` | PEP 440 |
109111

110-
112+
Note SemVer allows arbitrary pre-releases while PEP 440 only allows 3 kinds (a, b, rc).
113+
During conversion,
111114

112115
---
113116

@@ -146,6 +149,8 @@ These files are currently supported out-of-the-box:
146149
| `--r-description-path` | `r_description_path` | Path to R `DESCRIPTION` file | No | `DESCRIPTION` |
147150
| `--check-pom-xml` | `check_pom_xml` | Check `pom.xml`? (`true/false`) | No | `true` |
148151
| `--pom-xml-path` | `pom_xml_path` | Path to `pom.xml` | No | `pom.xml` |
152+
| `--check-nuspec` | `check_nu_spec` | Check `.nuspec`? (`true/false`) | No | `true` |
153+
| `--nuspec-path` | `nuspec_path` | Path to `.nuspec` | No | `.nuspec` |
149154

150155

151156

@@ -200,10 +205,10 @@ jobs:
200205
- name: Install Python
201206
uses: actions/setup-python@v5
202207
with:
203-
python-version: ">=3.12"
208+
python-version: ">=3.10"
204209

205210
- name: Run same-version
206-
uses: willynilly/same-version@v4.0.0
211+
uses: willynilly/same-version@v5.0.0
207212
with:
208213
fail_for_missing_file: false
209214
check_github_event: true
@@ -219,6 +224,7 @@ jobs:
219224
check_cargo_toml: false
220225
check_py_version_assignment: false
221226
check_pom_xml: false
227+
check_nuspec: false
222228
check_composer_json: false
223229
check_ro_crate_metadata_json: false
224230
```
@@ -248,10 +254,10 @@ jobs:
248254
- name: Install Python
249255
uses: actions/setup-python@v5
250256
with:
251-
python-version: ">=3.12"
257+
python-version: ">=3.10"
252258

253259
- name: Run same-version
254-
uses: willynilly/same-version@v4.0.0
260+
uses: willynilly/same-version@v5.0.0
255261
with:
256262
fail_for_missing_file: false
257263
check_github_event: true
@@ -267,6 +273,7 @@ jobs:
267273
check_cargo_toml: false
268274
check_py_version_assignment: false
269275
check_pom_xml: false
276+
check_nuspec: false
270277
check_composer_json: false
271278
check_ro_crate_metadata_json: false
272279
```
@@ -287,7 +294,7 @@ Add to your `.pre-commit-config.yaml`:
287294
```yaml
288295
repos:
289296
- repo: https://github.com/willynilly/same-version
290-
rev: v4.0.0 # Use latest tag
297+
rev: v5.0.0 # Use latest tag
291298
hooks:
292299
- id: same-version
293300
stages: [pre-commit, pre-push]
@@ -355,7 +362,7 @@ To set up your development environment:
355362
```bash
356363
git clone https://github.com/willynilly/same-version.git
357364
cd same-version
358-
pip install -e .
365+
pip install -e .[testing,dev]
359366
pre-commit install -t pre-commit -t pre-push
360367
pre-commit run --all-files
361368
```

example.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/willynilly/same-version
3-
rev: v4.0.0 # Use latest tag
3+
rev: v5.0.0 # Use latest tag
44
hooks:
55
- id: same-version
66
stages: [pre-commit, pre-push]

pyproject.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "same-version"
7-
version = "4.0.0"
7+
version = "5.0.0"
88
description = "Automatically ensures your software version metadata is consistent across key project files."
99
readme = "README.md"
10-
requires-python = ">=3.12"
10+
requires-python = ">=3.10"
1111
license = "Apache-2.0"
12-
license-files = ["LICEN[CS]E*"]
1312
keywords = ["open science", "FAIR", "version", "software quality", "DevOps", "CI/CD", "GitHub Action", "pyproject.toml", "package.json", "setup.py", "codemeta.json", ".zenodo.json", "Zenodo", "CodeMeta", "CITATION.cff", "CFF", "citation", "metadata", "harmonization"]
1413
authors = [
1514
{ name = "Will Riley", email = "wanderingwill@gmail.com" },
@@ -31,7 +30,7 @@ classifiers = [
3130
"Topic :: Software Development",
3231
"Topic :: Utilities"
3332
]
34-
dependencies = ["pyyaml>=6.0.2", "tomli>=2.2.1", "packaging>=25.0", "semver>=3.0.4"]
33+
dependencies = ["pyyaml>=6.0.2", "tomli>=2.2.1", "verple>=1.0.0"]
3534

3635
[project.urls]
3736
Homepage = "https://github.com/willynilly/same-version"
@@ -45,6 +44,7 @@ testing = [
4544
]
4645
dev = [
4746
"ruff>=0.11.12",
47+
"pre-commit>=4.2.0"
4848
]
4949

5050
[tool.pytest.ini_options]
@@ -54,6 +54,7 @@ pythonpath = [
5454

5555
[tool.hatch.build]
5656
include = ["src/same_version/**", "CITATION.cff"]
57+
license-files = ["LICEN[CS]E*"]
5758

5859
[tool.hatch.build.targets.wheel]
5960
packages = ["src/same_version"]

src/same_version/checkers/checker.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import logging
22
from argparse import Namespace
33

4-
import packaging.version
4+
from verple.verple import Verple
55

66
from same_version.extractors.extractor import Extractor
7-
from same_version.utils import parse_version_pep440
87

98
logger = logging.getLogger(__name__)
109

@@ -14,18 +13,23 @@ def __init__(self, extractor: Extractor, cli_args: Namespace):
1413
self.extractor = extractor
1514
self.cli_args = cli_args
1615
self._extracted_version: str | None = self.extractor.extract_version()
17-
18-
def create_pep440_version(self, version_str: str | None) -> packaging.version.Version | None:
19-
version_pep440 : packaging.version.Version | None = parse_version_pep440(version_str)
20-
return version_pep440
16+
self._verple_version: Verple | None = self.create_verple_version(self._extracted_version)
17+
18+
def create_verple_version(self, version_str: str | None) -> Verple | None:
19+
if version_str is None:
20+
return None
21+
try:
22+
return Verple.parse(version_str)
23+
except ValueError:
24+
return None
2125

2226
@property
2327
def target_version_str(self) -> str | None:
2428
return self._extracted_version
2529

2630
@property
27-
def target_version_pep440(self) -> packaging.version.Version | None:
28-
return self.create_pep440_version(version_str=self.target_version_str)
31+
def target_version(self) -> Verple | None:
32+
return self._verple_version
2933

3034
@property
3135
def target_exists(self) -> bool:
@@ -40,12 +44,11 @@ def target_cli_parameter_name(self) -> str | None:
4044
return self.extractor.target_cli_parameter_name
4145

4246
def check(self, base_version_str: str | None) -> bool:
43-
if not base_version_str:
47+
if base_version_str is None:
4448
return True
45-
base_version_pep440 = self.create_pep440_version(version_str=base_version_str)
49+
base_version: Verple | None = self.create_verple_version(version_str=base_version_str)
4650
if self.target_exists:
47-
48-
if base_version_pep440 != self.target_version_pep440:
51+
if base_version != self.target_version:
4952
self._log_version_mismatch(base_version_str=base_version_str)
5053
return False
5154
else:
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import logging
2+
from argparse import Namespace
3+
4+
from same_version.checkers.file_checker import FileChecker
5+
from same_version.extractors.nuspec_extractor import NuspecExtractor
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
class NuspecChecker(FileChecker):
11+
12+
def __init__(self, extractor: NuspecExtractor, cli_args: Namespace):
13+
super().__init__(extractor=extractor, cli_args=cli_args)
Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,12 @@
11
import logging
2+
from argparse import Namespace
23

34
from same_version.checkers.file_checker import FileChecker
4-
from same_version.utils import parse_version_semver
5+
from same_version.extractors.package_json_extractor import PackageJsonExtractor
56

67
logger = logging.getLogger(__name__)
78

89
class PackageJsonChecker(FileChecker):
9-
10-
def check(self, base_version_str: str | None) -> bool:
11-
if base_version_str is None:
12-
return True
13-
base_version_pep440 = self.create_pep440_version(version_str=base_version_str)
14-
if base_version_pep440 is None:
15-
return True
16-
17-
if self.target_exists:
18-
target_version_str = self.target_version_str
19-
target_name = self.target_name
20-
21-
try:
22-
base_version_semver_str: str = f"{base_version_pep440.major}.{base_version_pep440.minor}.{base_version_pep440.micro}"
23-
if base_version_pep440.is_prerelease and base_version_pep440.pre is not None:
24-
base_version_semver_str += f"-{base_version_pep440.pre[0]}.{base_version_pep440.pre[1]}"
25-
26-
base_version_semver = parse_version_semver(base_version_semver_str)
27-
target_version_semver = parse_version_semver(target_version_str)
28-
29-
if base_version_semver != target_version_semver:
30-
logger.error(f"❌ Version mismatch: {target_name} {target_version_str} != base version {base_version_str}")
31-
return False
32-
else:
33-
return True
34-
35-
except Exception as e:
36-
logger.error(f"❌ Parse error: Could not parse tag version as SemVer for {self.target_name}: {e}")
37-
return False
38-
else:
39-
self._log_missing_target()
40-
return False
10+
11+
def __init__(self, extractor: PackageJsonExtractor, cli_args: Namespace):
12+
super().__init__(extractor=extractor, cli_args=cli_args)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import xml.etree.ElementTree as ET
2+
from argparse import Namespace
3+
4+
from same_version.extractors.xml_extractor import XmlExtractor
5+
6+
7+
class NuspecExtractor(XmlExtractor):
8+
9+
def __init__(self, cli_args: Namespace):
10+
target_cli_parameter_name: str = '--nuspec-path'
11+
default_target_name: str = ".nuspec"
12+
super().__init__(
13+
target_file_path=self._create_target_file_path_from_cli_arg(cli_args=cli_args, cli_arg_parameter=target_cli_parameter_name),
14+
default_target_name=default_target_name,
15+
target_cli_parameter_name=target_cli_parameter_name
16+
)
17+
18+
def _get_version_from_data(self, data: dict) -> str | None:
19+
version: str | None = None
20+
tree: ET.ElementTree[ET.Element[str]] | None = data.get('tree', None)
21+
if tree is not None:
22+
23+
try:
24+
root = tree.getroot()
25+
26+
# XML structure: <package><metadata><version>...</version>
27+
# The {*} is used for optional namespaces that some tools prepend
28+
metadata = root.find('metadata') or root.find('{*}metadata')
29+
30+
if metadata is not None:
31+
version_elem = metadata.find('version') or metadata.find('{*}version')
32+
if version_elem is not None and version_elem.text:
33+
version = version_elem.text.strip()
34+
35+
except Exception:
36+
version = None
37+
38+
return version

src/same_version/main.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from same_version.checkers.codemeta_json_checker import CodeMetaJsonChecker
1111
from same_version.checkers.composer_json_checker import ComposerJsonChecker
1212
from same_version.checkers.github_event_checker import GitHubEventChecker
13+
from same_version.checkers.nuspec_checker import NuspecChecker
1314
from same_version.checkers.package_json_checker import PackageJsonChecker
1415
from same_version.checkers.pom_xml_checker import PomXmlChecker
1516
from same_version.checkers.py_version_assignment_checker import (
@@ -28,6 +29,7 @@
2829
from same_version.extractors.codemeta_json_extractor import CodeMetaJsonExtractor
2930
from same_version.extractors.composer_json_extractor import ComposerJsonExtractor
3031
from same_version.extractors.github_event_extractor import GitHubEventExtractor
32+
from same_version.extractors.nuspec_extractor import NuspecExtractor
3133
from same_version.extractors.package_json_extractor import PackageJsonExtractor
3234
from same_version.extractors.pom_xml_extractor import PomXmlExtractor
3335
from same_version.extractors.py_version_assignment_extractor import (
@@ -72,6 +74,9 @@ def parse_args() -> argparse.Namespace:
7274
parser.add_argument('--check-pom-xml', default=True, required=False, help='Check pom.xml? (true/false)')
7375
parser.add_argument('--pom-xml-path', default='pom.xml', required=False, help='Path to pom.xml')
7476

77+
parser.add_argument('--check-nuspec', default=True, required=False, help='Check .nuspec? (true/false)')
78+
parser.add_argument('--nuspec-path', default='.nuspec', required=False, help='Path to .nuspec')
79+
7580
parser.add_argument('--check-cargo-toml', default=True, required=False, help='Check Cargo.toml? (true/false)')
7681
parser.add_argument('--cargo-toml-path', default='Cargo.toml', required=False, help='Path to Cargo.toml')
7782

@@ -156,6 +161,12 @@ def main():
156161
pom_xml_checker: PomXmlChecker = PomXmlChecker(extractor=pom_xml_extractor, cli_args=cli_args)
157162
checkers.append(pom_xml_checker)
158163

164+
# .nuspec
165+
if str(getattr(cli_args, 'check_nuspec', '') or '').lower() == 'true':
166+
nuspec_extractor: NuspecExtractor = NuspecExtractor(cli_args=cli_args)
167+
nuspec_checker: NuspecChecker = NuspecChecker(extractor=nuspec_extractor, cli_args=cli_args)
168+
checkers.append(nuspec_checker)
169+
159170
# R DESCRIPTION file
160171
if str(getattr(cli_args, 'check_r_description', '') or '').lower() == 'true':
161172
r_description_extractor: RDescriptionExtractor = RDescriptionExtractor(cli_args=cli_args)

0 commit comments

Comments
 (0)