diff --git a/README.md b/README.md index 79aece1..3b5f80e 100644 --- a/README.md +++ b/README.md @@ -76,16 +76,17 @@ accepts the common version parts: - `patch` - `pre-release` -e.g. `1.2.3-rc0`. +e.g. `1.2.3-rc.0`. Note that where normalisation occurs, the round-trip result will differ. This can be avoided by careful choice of the delimeters e.g. `-.`. ### Version source options -| Option | Type | Default | Description | -|---------------| --- |---------------|--------------------------------------------| -| `path` | `str` | `package.json` | Relative path to the `package.json` file. | +| Option | Type | Default | Description | +|---------------|--------|----------------|-----------------------------------------------------| +| `path` | `str` | `package.json` | Relative path to the `package.json` file. | +| `canonical` | `bool` | `True` | Whether to convert Python prerelease string or not. | ## Metadata hook diff --git a/hatch_nodejs_version/version_source.py b/hatch_nodejs_version/version_source.py index fedda64..280ef27 100644 --- a/hatch_nodejs_version/version_source.py +++ b/hatch_nodejs_version/version_source.py @@ -7,6 +7,19 @@ from hatchling.version.source.plugin.interface import VersionSourceInterface +# Python to semver pre-release spelling +# See https://peps.python.org/pep-0440/#pre-release-spelling +PEP_440_TO_SEMVER_PRERELEASE = { + "alpha": "alpha", + "a": "alpha", + "beta": "beta", + "b": "beta", + "rc": "rc", + "c": "rc", + "pre": "rc", + "preview": "rc", +} + # The Python-aware NodeJS version regex # This is very similar to `packaging.version.VERSION_PATTERN`, with a few changes: # - Don't accept underscores @@ -21,8 +34,7 @@ (?P
# pre-release - (?P(a|b|c|rc|alpha|beta|pre|preview)) - [-\.]? - (?P [0-9]+)? + (\.(?P [0-9]+))? )? (?: \+ @@ -78,6 +90,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__path = None + self.__canonical = None @property def path(self): @@ -94,6 +107,13 @@ def path(self): return self.__path + @property + def canonical(self) -> bool: + """Whether the Node pre-release version is converted or not.""" + if self.__canonical is None: + self.__canonical = self.config.get("canonical", True) + return self.__canonical + @staticmethod def node_version_to_python(version: str) -> str: # NodeJS version strings are a near superset of Python version strings @@ -119,7 +139,7 @@ def node_version_to_python(version: str) -> str: return "".join(parts) @staticmethod - def python_version_to_node(version: str) -> str: + def python_version_to_node(version: str, canonical: bool = True) -> str: # NodeJS version strings are a near superset of Python version strings match = re.match( r"^\s*" + PYTHON_VERSION_PATTERN + r"\s*$", @@ -132,10 +152,16 @@ def python_version_to_node(version: str) -> str: parts = ["{major}.{minor}.{patch}".format_map(match)] if match["pre"]: + pre = "-" + if canonical: + pre += PEP_440_TO_SEMVER_PRERELEASE.get(match["pre_l"], match["pre_l"]) + else: + pre += match["pre_l"] if match["pre_n"] is None: - parts.append("-{pre_l}".format_map(match)) + parts.append(pre) else: - parts.append("-{pre_l}{pre_n}".format_map(match)) + pre_n = match["pre_n"] + parts.append(f"{pre}.{pre_n}") if match["local"]: parts.append("+{local}".format_map(match)) @@ -162,8 +188,7 @@ def set_version(self, version: str, version_data): raw_data = f.read() data = json.loads(raw_data) - - data["version"] = self.python_version_to_node(version) + data["version"] = self.python_version_to_node(version, self.canonical) with open(path, "w") as f: json.dump(data, f, indent=4) if raw_data.endswith('\n'): diff --git a/tests/test_version_config.py b/tests/test_version_config.py index 595647a..97f4796 100644 --- a/tests/test_version_config.py +++ b/tests/test_version_config.py @@ -9,18 +9,34 @@ GOOD_NODE_PYTHON_VERSIONS = [ ("1.4.5", "1.4.5"), - ("1.4.5-a0", "1.4.5a0"), + ("1.4.5-a.0", "1.4.5a0"), ("1.4.5-a", "1.4.5a"), - ("1.4.5-b0", "1.4.5b0"), - ("1.4.5-c1", "1.4.5c1"), - ("1.4.5-rc0", "1.4.5rc0"), - ("1.4.5-alpha0", "1.4.5alpha0"), - ("1.4.5-beta0", "1.4.5beta0"), - ("1.4.5-pre9", "1.4.5pre9"), - ("1.4.5-preview0", "1.4.5preview0"), - ("1.4.5-preview0+build1.0.0", "1.4.5preview0+build1.0.0"), - ("1.4.5-preview0+build-1.0.0", "1.4.5preview0+build-1.0.0"), - ("1.4.5-preview0+good-1_0.0", "1.4.5preview0+good-1_0.0"), + ("1.4.5-b.0", "1.4.5b0"), + ("1.4.5-c.1", "1.4.5c1"), + ("1.4.5-rc.0", "1.4.5rc0"), + ("1.4.5-alpha.0", "1.4.5alpha0"), + ("1.4.5-beta.0", "1.4.5beta0"), + ("1.4.5-pre.9", "1.4.5pre9"), + ("1.4.5-preview.0", "1.4.5preview0"), + ("1.4.5-preview.0+build1.0.0", "1.4.5preview0+build1.0.0"), + ("1.4.5-preview.0+build-1.0.0", "1.4.5preview0+build-1.0.0"), + ("1.4.5-preview.0+good-1_0.0", "1.4.5preview0+good-1_0.0"), +] + +GOOD_CANONICAL_NODE_PYTHON_VERSIONS = [ + ("1.4.5", "1.4.5"), + ("1.4.5-alpha.0", "1.4.5a0"), + ("1.4.5-alpha", "1.4.5a"), + ("1.4.5-beta.0", "1.4.5b0"), + ("1.4.5-rc.1", "1.4.5c1"), + ("1.4.5-rc.0", "1.4.5rc0"), + ("1.4.5-alpha.0", "1.4.5alpha0"), + ("1.4.5-beta.0", "1.4.5beta0"), + ("1.4.5-rc.9", "1.4.5pre9"), + ("1.4.5-rc.0", "1.4.5preview0"), + ("1.4.5-rc.0+build1.0.0", "1.4.5preview0+build1.0.0"), + ("1.4.5-rc.0+build-1.0.0", "1.4.5preview0+build-1.0.0"), + ("1.4.5-rc.0+good-1_0.0", "1.4.5preview0+good-1_0.0"), ] @@ -44,11 +60,13 @@ def test_parse_python_incorrect(self, python_version): "node_version", [ "1.4", + "1.4.5a.0", "1.4.5a0", - "1.4.5-c0.post1", - "1.4.5-rc0.post1.dev2", - "1.4.5-rc0.post1+-bad", - "1.4.5-rc0.post1+bad_", + "1.4.5-a0", + "1.4.5-c.0.post1", + "1.4.5-rc.0.post1.dev2", + "1.4.5-rc.0.post1+-bad", + "1.4.5-rc.0.post1+bad_", ], ) def test_parse_node_incorrect(self, node_version): @@ -75,8 +93,6 @@ def test_version_from_package( [project] name = "my-app" dynamic = ["version"] -[tool.hatch.version] -source = "nodejs" """ ) package_json = "package.json" if alt_package_json is None else alt_package_json @@ -126,9 +142,53 @@ def test_version_to_package( """ ) config = {} if alt_package_json is None else {"path": alt_package_json} + config["canonical"] = False + version_source = NodeJSVersionSource(project, config=config) + version_data = version_source.get_version_data() + version_source.set_version(python_version, version_data) + + written_package = json.loads((project / package_json).read_text()) + assert written_package["version"] == node_version + + @pytest.mark.parametrize( + "node_version, python_version", + GOOD_CANONICAL_NODE_PYTHON_VERSIONS, + ) + @pytest.mark.parametrize( + "alt_package_json", + [None, "package-other.json"], + ) + def test_canonical_version_to_package( + self, project, node_version, python_version, alt_package_json + ): + package_json = "package.json" if alt_package_json is None else alt_package_json + (project / "pyproject.toml").write_text( + """ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" +[project] +name = "my-app" +dynamic = ["version"] +[tool.hatch.version] +source = "nodejs" +canonical = True +""" + ) + (project / package_json).write_text( + """ +{ + "name": "my-app", + "version": "0.0.0" +} +""" + ) + config = {} if alt_package_json is None else {"path": alt_package_json} + config["canonical"] = True version_source = NodeJSVersionSource(project, config=config) version_data = version_source.get_version_data() version_source.set_version(python_version, version_data) written_package = json.loads((project / package_json).read_text()) assert written_package["version"] == node_version +