Skip to content

Add option to canonicalise the version #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ Note that where normalisation occurs, the round-trip result will differ. This ca

### 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` | `str` | `False` | Whether to convert Python prerelease string or not. |

## Metadata hook

Expand Down
37 changes: 32 additions & 5 deletions hatch_nodejs_version/version_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
CANONICAL_MAPPING = {
"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
Expand Down Expand Up @@ -78,6 +91,7 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.__path = None
self.__canonical = None

@property
def path(self):
Expand All @@ -94,6 +108,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", False)
return self.__canonical

@staticmethod
def node_version_to_python(version: str) -> str:
# NodeJS version strings are a near superset of Python version strings
Expand All @@ -119,7 +140,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 = False) -> str:
# NodeJS version strings are a near superset of Python version strings
match = re.match(
r"^\s*" + PYTHON_VERSION_PATTERN + r"\s*$",
Expand All @@ -132,10 +153,16 @@ def python_version_to_node(version: str) -> str:
parts = ["{major}.{minor}.{patch}".format_map(match)]

if match["pre"]:
pre = "-"
if canonical:
pre += CANONICAL_MAPPING.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))
Expand All @@ -162,8 +189,8 @@ 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)
print(self.__canonical, version, self.python_version_to_node(version, self.canonical))
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'):
Expand Down
83 changes: 70 additions & 13 deletions tests/test_version_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,34 @@

GOOD_NODE_PYTHON_VERSIONS = [
("1.4.5", "1.4.5"),
("1.4.5-a0", "1.4.5a0"),
Copy link
Collaborator

@agoose77 agoose77 Jan 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right - a0 is parsed by semver as the pre-release named "a0", not "alpha" version "0". Good catch!

In fact, this shouldn't successfully parse. Can you change the NodeJS REGEX to make the . required if a pre-release numeral is given? i.e. (probably not this, but approximately)

(?P<pre_n>\.[0-9]+)?

This would mean that pre_n includes the delimiter in the match results.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current regex is

(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
        [-\.]?
        (?P<pre_n>[0-9]+)?

Is it ok to drop the dash? d2139b3 assumes so.

Copy link
Collaborator

@agoose77 agoose77 Jan 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be explicit, these changes are to the general regex, rather than just the canonicalisation logic you're proposing.

  1. For Python's sake, I think we probably need to enforce a digit here, because semver treats 1.0.0-a < 1.0.0-a0. This means that these two versions are distinct for Node.js, but degenerate for Python.
  2. We need to drop the - because it would be considered part of the pre-release identifier, i.e. a|b|c|rc|alpha|beta|pre|preview, rather than an actual separator

I suppose the new pattern would be

(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
        \.
        (?P<pre_n>[0-9]+)

I'm using

for reference, but they're complex documents, so it's possible I will make mistakes. Let me know if you disagree!

("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"),
]


Expand Down Expand Up @@ -75,8 +91,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
Expand Down Expand Up @@ -132,3 +146,46 @@ def test_version_to_package(

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