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
+