Skip to content
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

Handle additional path-based version specifiers #25

Merged
Show file tree
Hide file tree
Changes from all 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
68 changes: 59 additions & 9 deletions pyarn/lockfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
import json
import logging
import re
from typing import Pattern
from pathlib import Path
from typing import Any, Dict, Optional, Pattern

from ply import lex, yacc

Expand All @@ -34,8 +35,16 @@

class Package:
def __init__(
self, name, version, url=None, checksum=None, relpath=None, dependencies=None, alias=None
):
self,
name: str,
version: str,
url: Optional[str] = None,
checksum: Optional[str] = None,
path: Optional[str] = None,
relpath: Optional[str] = None,
dependencies: Optional[Dict[str, str]] = None,
alias: Optional[str] = None,
) -> None:
if not name:
raise ValueError("Package name was not provided")

Expand All @@ -46,12 +55,32 @@ def __init__(
self.version = version
self.url = url
self.checksum = checksum
self.relpath = relpath
self.path = path if path is not None else relpath
self.dependencies = dependencies or {}
self.alias = alias

@property
def relpath(self) -> Optional[str]:

Choose a reason for hiding this comment

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

Please add a note stating that this is deprecated and is left for backward compatibility only.

"""
Return the path to the package.

This is strictly kept for backwards compatibility and path should be used directly
instead. The path is not always relative and may be absolute.
"""
return self.path

@relpath.setter
def relpath(self, path: Optional[str]) -> None:
"""
Set the path to the package.

This is strictly kept for backwards compatibility and path should be used directly
instead. The path is not always relative and may be absolute.
"""
self.path = path

@classmethod
def from_dict(cls, raw_name, data):
def from_dict(cls, raw_name: str, data: Dict[str, Any]) -> "Package":
name_at_version = re.compile(r"(?P<name>@?[^@]+)(?:@(?P<version>[^,]*))?")

# _version is the version as declared in package.json, not the resolved version
Expand All @@ -63,19 +92,40 @@ def from_dict(cls, raw_name, data):
alias = name
name, _version = _must_match(name_at_version, _remove_prefix(_version, "npm:")).groups()

if _version and _version.startswith("file:"):
path = _remove_prefix(_version, "file:")
if _version:
path = cls.get_path_from_version_specifier(_version)

# Ensure the resolved version key exists to appease mypy
version = data.get("version")
if not version:
raise ValueError("Package version was not provided")

return cls(
name=name,
version=data.get("version"),
version=version,
url=data.get("resolved"),
checksum=data.get("integrity"),
relpath=path,
path=path,
dependencies=data.get("dependencies", {}),
alias=alias,
)

@staticmethod
def get_path_from_version_specifier(version: str) -> Optional[str]:
"""Return the path from a package.json file dependency version specifier."""
version_path = Path(version)

if version.startswith("file:"):
return _remove_prefix(version, "file:")
elif version.startswith("link:"):
return _remove_prefix(version, "link:")
elif version_path.is_absolute() or version.startswith(("./", "../")):
return str(version_path)
else:
# Some non-path version specifier, (e.g. "1.0.0" or a web link)
# See https://docs.npmjs.com/cli/v10/configuring-npm/package-json#dependencies
return None

Choose a reason for hiding this comment

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

A minor, non-blocking suggestion: could you please add a short comment or a link to a doc about when this branch could be reached?



def _remove_prefix(s: str, prefix: str) -> str:
return s[len(prefix) :]
Expand Down
61 changes: 48 additions & 13 deletions tests/test_lockfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,19 +120,24 @@ def test_packages():
assert packages[0].version == "2.0.0"
assert packages[0].checksum is None
assert packages[0].url is None
assert packages[0].relpath is None
assert packages[0].path is None


def test_packages_no_version():
def test_lock_packages_no_version():
data = "breakfast@^1.1.1:\n eggs bacon"
lock = lockfile.Lockfile.from_str(data)
with pytest.raises(ValueError, match="Package version was not provided"):
lock.packages()


def test_package_no_version():
with pytest.raises(ValueError, match="Package version was not provided"):
lockfile.Package("breakfast", None) # type: ignore[arg-type]


def test_packages_no_name():
with pytest.raises(ValueError, match="Package name was not provided"):
lockfile.Package(None, "1.0.0")
lockfile.Package(None, "1.0.0") # type: ignore[arg-type]


def test_packages_url():
Expand All @@ -145,7 +150,7 @@ def test_packages_url():
assert packages[0].version == "2.0.0"
assert packages[0].checksum is None
assert packages[0].url == url
assert packages[0].relpath is None
assert packages[0].path is None


def test_packages_checksum():
Expand All @@ -158,19 +163,49 @@ def test_packages_checksum():
assert packages[0].version == "2.0.0"
assert packages[0].checksum == "someHash"
assert packages[0].url == url
assert packages[0].relpath is None
assert packages[0].path is None


def test_relpath():
data = '"breakfast@file:some/relative/path":\n version "0.0.0"'
@pytest.mark.parametrize(
"data, expected_path",
[
pytest.param(
'"breakfast@file:some/relative/path":\n version "0.0.0"',
"some/relative/path",
id="relpath_with_file_prefix",
),
pytest.param(
'"breakfast@link:some/relative/path":\n version "0.0.0"',
"some/relative/path",
id="relpath_with_link_prefix",
),
pytest.param(
'"breakfast@./some/relative/path":\n version "0.0.0"',
"some/relative/path",
id="relpath_with_dot_prefix",
),
pytest.param(
'"breakfast@../some/relative/path":\n version "0.0.0"',
"../some/relative/path",
id="relpath_to_parent_dir",
),
pytest.param(
'"breakfast@/some/absolute/path":\n version "0.0.0"',
"/some/absolute/path",
id="absolute_path",
),
],
)
def test_package_with_path(data: str, expected_path: str) -> None:
lock = lockfile.Lockfile.from_str(data)
packages = lock.packages()
assert len(packages) == 1
assert packages[0].name == "breakfast"
assert packages[0].version == "0.0.0"
assert packages[0].checksum is None
assert packages[0].url is None
assert packages[0].relpath == "some/relative/path"
assert packages[0].path == expected_path
assert packages[0].relpath == expected_path # test backwards compatibility


def test_package_with_comma():
Expand All @@ -182,7 +217,7 @@ def test_package_with_comma():
assert packages[0].version == "1.1.7"
assert packages[0].checksum is None
assert packages[0].url is None
assert packages[0].relpath is None
assert packages[0].path is None


DATA_TO_DUMP = {
Expand Down Expand Up @@ -298,19 +333,19 @@ def test_aliased_packages(test_data_dir: Path):
assert babel.name == "babel-plugin-add-module-exports"
assert babel.alias == "babel7-plugin-add-module-exports"
assert babel.version == "1.0.0"
assert babel.relpath is None
assert babel.path is None

assert lodash.name == "@elastic/lodash"
assert lodash.alias == "lodash"
assert lodash.version == "3.10.1-kibana1"
assert lodash.relpath is None
assert lodash.path is None

assert fecha.name == "fecha"
assert fecha.alias == "date-in-spanish"
assert fecha.version == "4.2.3"
assert fecha.relpath is None
assert fecha.path is None

assert nonsense.name == "what"
assert nonsense.alias == "probably-nonsense"
assert nonsense.version == "0.0.1"
assert nonsense.relpath == "how"
assert nonsense.path == "how"
Loading