Skip to content

Commit

Permalink
yarn v1: Update workspace metadata extraction
Browse files Browse the repository at this point in the history
At the time of implementing Workspace model, there
was no PackageJson class, therefore only the raw
dictionary was availible for use inside the model.

Replace raw dictionary access for package.json data with the PackageJson
class to enhance type safety and readability. The Workspace model now
uses PackageJson instead of a dictionary for package_contents, with
updated validators.

Signed-off-by: Michal Šoltis <[email protected]>
  • Loading branch information
slimreaper35 committed Dec 3, 2024
1 parent 7e5f9dc commit 432a7e1
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 33 deletions.
4 changes: 2 additions & 2 deletions cachi2/core/package_managers/yarn_classic/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ def _get_workspace_packages(
"""Return a WorkspacePackage for each Workspace."""
return [
WorkspacePackage(
name=ws.package_contents["name"],
version=ws.package_contents.get("version"),
name=ws.package_json.data.get("name"),
version=ws.package_json.data.get("version"),
relpath=ws.path.relative_to(source_dir.path),
)
for ws in workspaces
Expand Down
35 changes: 22 additions & 13 deletions cachi2/core/package_managers/yarn_classic/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,27 @@
import pydantic

from cachi2.core.errors import PackageRejected
from cachi2.core.package_managers.yarn_classic.project import PackageJson
from cachi2.core.rooted_path import RootedPath


class Workspace(pydantic.BaseModel):
"""Workspace model."""
"""
Workspace model.
Attributes:
path: Path to workspace directory.
package_json: Content of package.json file.
"""

path: Path # path to a workspace.
package_contents: dict # package data extracted from path/"package.json".
path: Path
package_json: PackageJson

@pydantic.field_validator("package_contents")
def _ensure_package_is_named(cls, package_contents: dict) -> dict:
if "name" not in package_contents:
@pydantic.field_validator("package_json")
def _ensure_package_is_named(cls, package_json: PackageJson) -> PackageJson:
if "name" not in package_json.data:
raise ValueError("Workspaces must contain 'name' field.")
return package_contents
return package_json


def ensure_no_path_leads_out(
Expand Down Expand Up @@ -83,21 +90,23 @@ def _read_package_from(path: RootedPath) -> dict[str, Any]:
return json.loads(path.join_within_root("package.json").path.read_text())


def extract_workspace_metadata(
package_path: RootedPath,
) -> list[Workspace]:
def extract_workspace_metadata(package_path: RootedPath) -> list[Workspace]:
"""Extract workspace metadata from a package."""
processed_package = _read_package_from(package_path)
workspaces_globs = _extract_workspaces_globs(processed_package)
package_json = PackageJson.from_file(package_path.join_within_root("package.json"))
workspaces_globs = _extract_workspaces_globs(package_json.data)
workspaces_paths = _get_workspace_paths(workspaces_globs, package_path)
ensure_no_path_leads_out(workspaces_paths, package_path)
_ensure_workspaces_are_well_formed(workspaces_paths)

parsed_workspaces = []
for wp in workspaces_paths:
parsed_workspaces.append(
Workspace(
path=wp,
package_contents=_read_package_from(package_path.join_within_root(wp)),
package_json=PackageJson.from_file(
package_path.join_within_root(wp, "package.json")
),
)
)

return parsed_workspaces
15 changes: 11 additions & 4 deletions tests/unit/package_managers/yarn_classic/test_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,16 +324,23 @@ def test__get_main_package_no_name(rooted_tmp_path: RootedPath) -> None:


def test__get_workspace_packages(rooted_tmp_path: RootedPath) -> None:
workspace_path = rooted_tmp_path.join_within_root("foo").path
workspace_path = rooted_tmp_path.join_within_root("foo")
workspace_path.path.mkdir()

package_json_path = workspace_path.join_within_root("package.json")
package_json_path.path.write_text('{"name": "foo", "version": "1.0.0"}')

package_json = PackageJson.from_file(package_json_path)
workspace = Workspace(
path=workspace_path,
package_contents={"name": "foo", "version": "1.0.0"},
path=workspace_path.path,
package_json=package_json,
)

expected = [
WorkspacePackage(
name="foo",
version="1.0.0",
relpath=workspace_path.relative_to(rooted_tmp_path.path),
relpath=workspace_path.path.relative_to(rooted_tmp_path.path),
)
]

Expand Down
33 changes: 19 additions & 14 deletions tests/unit/package_managers/yarn_classic/test_workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest

from cachi2.core.errors import PathOutsideRoot
from cachi2.core.package_managers.yarn_classic.project import PackageJson
from cachi2.core.package_managers.yarn_classic.workspaces import (
Workspace,
_extract_workspaces_globs,
Expand All @@ -13,41 +14,45 @@
from cachi2.core.rooted_path import RootedPath


@mock.patch("cachi2.core.package_managers.yarn_classic.workspaces._read_package_from")
@mock.patch("cachi2.core.package_managers.yarn_classic.workspaces._get_workspace_paths")
def test_packages_with_workspaces_outside_source_dir_are_rejected(
mock_get_ws_paths: mock.Mock,
mock_read_package_from: mock.Mock,
rooted_tmp_path: RootedPath,
) -> None:
mock_read_package_from.return_value = {"workspaces": ["../../usr"]}
mock_get_ws_paths.return_value = [Path("/tmp/foo/bar"), Path("/usr")]
package_path = RootedPath("/tmp/foo")
package_json_path = rooted_tmp_path.join_within_root("package.json")
package_json_path.path.write_text('{"workspaces": ["../../usr"]}')
mock_get_ws_paths.return_value = [Path("/usr")]

with pytest.raises(PathOutsideRoot):
extract_workspace_metadata(package_path)
extract_workspace_metadata(rooted_tmp_path)


@mock.patch("cachi2.core.package_managers.yarn_classic.workspaces._read_package_from")
@mock.patch("cachi2.core.package_managers.yarn_classic.workspaces._get_workspace_paths")
@mock.patch(
"cachi2.core.package_managers.yarn_classic.workspaces._ensure_workspaces_are_well_formed"
)
def test_workspaces_could_be_parsed(
mock_workspaces_ok: mock.Mock,
mock_get_ws_paths: mock.Mock,
mock_read_package_from: mock.Mock,
rooted_tmp_path: RootedPath,
) -> None:
mock_read_package_from.side_effect = [{"workspaces": ["quux"]}, {"name": "inner_package"}]
mock_get_ws_paths.return_value = [Path("/tmp/foo/bar")]
package_path = RootedPath("/tmp/foo")
package_json_path = rooted_tmp_path.join_within_root("package.json")
package_json_path.path.write_text('{"name": "outer_package", "workspaces": ["foo"]}')

workspace_path = rooted_tmp_path.join_within_root("foo")
workspace_path.path.mkdir()
workspace_package_json_path = workspace_path.join_within_root("package.json")
workspace_package_json_path.path.write_text('{"name": "inner_package"}')

mock_get_ws_paths.return_value = [workspace_path.path]

expected_result = [
Workspace(
path="/tmp/foo/bar",
package_contents={"name": "inner_package"},
path=workspace_path.path,
package_json=PackageJson.from_file(workspace_package_json_path),
),
]
result = extract_workspace_metadata(package_path)
result = extract_workspace_metadata(rooted_tmp_path)

assert result == expected_result

Expand Down

0 comments on commit 432a7e1

Please sign in to comment.