pymelos 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pymelos/__init__.py +63 -0
- pymelos/__main__.py +6 -0
- pymelos/cli/__init__.py +5 -0
- pymelos/cli/__main__.py +6 -0
- pymelos/cli/app.py +527 -0
- pymelos/cli/commands/__init__.py +1 -0
- pymelos/cli/commands/init.py +151 -0
- pymelos/commands/__init__.py +84 -0
- pymelos/commands/add.py +77 -0
- pymelos/commands/base.py +108 -0
- pymelos/commands/bootstrap.py +154 -0
- pymelos/commands/changed.py +161 -0
- pymelos/commands/clean.py +142 -0
- pymelos/commands/exec.py +116 -0
- pymelos/commands/list.py +128 -0
- pymelos/commands/release.py +258 -0
- pymelos/commands/run.py +160 -0
- pymelos/compat.py +14 -0
- pymelos/config/__init__.py +47 -0
- pymelos/config/loader.py +132 -0
- pymelos/config/schema.py +236 -0
- pymelos/errors.py +139 -0
- pymelos/execution/__init__.py +32 -0
- pymelos/execution/parallel.py +249 -0
- pymelos/execution/results.py +172 -0
- pymelos/execution/runner.py +171 -0
- pymelos/filters/__init__.py +27 -0
- pymelos/filters/chain.py +101 -0
- pymelos/filters/ignore.py +60 -0
- pymelos/filters/scope.py +90 -0
- pymelos/filters/since.py +98 -0
- pymelos/git/__init__.py +69 -0
- pymelos/git/changes.py +153 -0
- pymelos/git/commits.py +174 -0
- pymelos/git/repo.py +210 -0
- pymelos/git/tags.py +242 -0
- pymelos/py.typed +0 -0
- pymelos/types.py +16 -0
- pymelos/uv/__init__.py +44 -0
- pymelos/uv/client.py +167 -0
- pymelos/uv/publish.py +162 -0
- pymelos/uv/sync.py +168 -0
- pymelos/versioning/__init__.py +57 -0
- pymelos/versioning/changelog.py +189 -0
- pymelos/versioning/conventional.py +216 -0
- pymelos/versioning/semver.py +249 -0
- pymelos/versioning/updater.py +146 -0
- pymelos/workspace/__init__.py +33 -0
- pymelos/workspace/discovery.py +138 -0
- pymelos/workspace/graph.py +238 -0
- pymelos/workspace/package.py +191 -0
- pymelos/workspace/workspace.py +218 -0
- pymelos-0.1.3.dist-info/METADATA +106 -0
- pymelos-0.1.3.dist-info/RECORD +57 -0
- pymelos-0.1.3.dist-info/WHEEL +4 -0
- pymelos-0.1.3.dist-info/entry_points.txt +2 -0
- pymelos-0.1.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Init command implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pymelos.errors import ConfigurationError
|
|
10
|
+
|
|
11
|
+
DEFAULT_PYMELOS_YAML = """# pymelos workspace configuration
|
|
12
|
+
name: {name}
|
|
13
|
+
|
|
14
|
+
packages:
|
|
15
|
+
- packages/*
|
|
16
|
+
|
|
17
|
+
scripts:
|
|
18
|
+
test:
|
|
19
|
+
run: pytest tests/ -v
|
|
20
|
+
description: Run tests
|
|
21
|
+
|
|
22
|
+
lint:
|
|
23
|
+
run: ruff check .
|
|
24
|
+
description: Run linting
|
|
25
|
+
|
|
26
|
+
format:
|
|
27
|
+
run: ruff format .
|
|
28
|
+
description: Format code
|
|
29
|
+
|
|
30
|
+
typecheck:
|
|
31
|
+
run: type check
|
|
32
|
+
description: Run type checking
|
|
33
|
+
|
|
34
|
+
command_defaults:
|
|
35
|
+
concurrency: 4
|
|
36
|
+
fail_fast: false
|
|
37
|
+
topological: true
|
|
38
|
+
|
|
39
|
+
clean:
|
|
40
|
+
patterns:
|
|
41
|
+
- "__pycache__"
|
|
42
|
+
- "*.pyc"
|
|
43
|
+
- ".pytest_cache"
|
|
44
|
+
- ".mypy_cache"
|
|
45
|
+
- ".ruff_cache"
|
|
46
|
+
- "*.egg-info"
|
|
47
|
+
- "dist"
|
|
48
|
+
- "build"
|
|
49
|
+
protected:
|
|
50
|
+
- ".venv"
|
|
51
|
+
- ".git"
|
|
52
|
+
|
|
53
|
+
versioning:
|
|
54
|
+
commit_format: conventional
|
|
55
|
+
tag_format: "{{name}}@{{version}}"
|
|
56
|
+
changelog:
|
|
57
|
+
enabled: true
|
|
58
|
+
filename: CHANGELOG.md
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
DEFAULT_PYPROJECT_TOML = """[project]
|
|
62
|
+
name = "{name}"
|
|
63
|
+
version = "0.0.0"
|
|
64
|
+
description = "Python monorepo"
|
|
65
|
+
requires-python = ">=3.12"
|
|
66
|
+
|
|
67
|
+
[tool.uv]
|
|
68
|
+
workspace = {{ members = ["packages/*"] }}
|
|
69
|
+
|
|
70
|
+
dev-dependencies = [
|
|
71
|
+
"pytest>=8.0.0",
|
|
72
|
+
"ruff>=0.8.0",
|
|
73
|
+
"mypy>=1.10.0",
|
|
74
|
+
]
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def init_workspace(path: Path, name: str | None = None) -> None:
|
|
79
|
+
"""Initialize a new pymelos workspace.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
path: Directory to initialize.
|
|
83
|
+
name: Workspace name (defaults to directory name).
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ConfigurationError: If workspace already exists.
|
|
87
|
+
"""
|
|
88
|
+
path = path.resolve()
|
|
89
|
+
|
|
90
|
+
if not path.exists():
|
|
91
|
+
path.mkdir(parents=True)
|
|
92
|
+
|
|
93
|
+
# Check if already initialized
|
|
94
|
+
if (path / "pymelos.yaml").exists():
|
|
95
|
+
raise ConfigurationError("Workspace already initialized", path=path / "pymelos.yaml")
|
|
96
|
+
|
|
97
|
+
# Use directory name as default
|
|
98
|
+
if not name:
|
|
99
|
+
name = path.name
|
|
100
|
+
|
|
101
|
+
# Create pymelos.yaml
|
|
102
|
+
pymelos_yaml = path / "pymelos.yaml"
|
|
103
|
+
pymelos_yaml.write_text(DEFAULT_PYMELOS_YAML.format(name=name), encoding="utf-8")
|
|
104
|
+
|
|
105
|
+
# Create pyproject.toml if it doesn't exist
|
|
106
|
+
pyproject = path / "pyproject.toml"
|
|
107
|
+
if not pyproject.exists():
|
|
108
|
+
pyproject.write_text(DEFAULT_PYPROJECT_TOML.format(name=name), encoding="utf-8")
|
|
109
|
+
|
|
110
|
+
# Create packages directory
|
|
111
|
+
packages_dir = path / "packages"
|
|
112
|
+
packages_dir.mkdir(exist_ok=True)
|
|
113
|
+
|
|
114
|
+
# Create .gitignore if it doesn't exist
|
|
115
|
+
gitignore = path / ".gitignore"
|
|
116
|
+
if not gitignore.exists():
|
|
117
|
+
gitignore.write_text(
|
|
118
|
+
"""# Python
|
|
119
|
+
__pycache__/
|
|
120
|
+
*.py[cod]
|
|
121
|
+
*.so
|
|
122
|
+
.venv/
|
|
123
|
+
dist/
|
|
124
|
+
build/
|
|
125
|
+
*.egg-info/
|
|
126
|
+
|
|
127
|
+
# Testing
|
|
128
|
+
.pytest_cache/
|
|
129
|
+
.coverage
|
|
130
|
+
htmlcov/
|
|
131
|
+
|
|
132
|
+
# Type checking
|
|
133
|
+
.mypy_cache/
|
|
134
|
+
|
|
135
|
+
# Linting
|
|
136
|
+
.ruff_cache/
|
|
137
|
+
|
|
138
|
+
# IDE
|
|
139
|
+
.vscode/
|
|
140
|
+
.idea/
|
|
141
|
+
|
|
142
|
+
# OS
|
|
143
|
+
.DS_Store
|
|
144
|
+
""",
|
|
145
|
+
encoding="utf-8",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Initialize git if not already a repo
|
|
149
|
+
if not (path / ".git").exists():
|
|
150
|
+
with contextlib.suppress(subprocess.CalledProcessError, FileNotFoundError):
|
|
151
|
+
subprocess.run(["git", "init"], cwd=path, capture_output=True, check=True)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""pymelos commands."""
|
|
2
|
+
|
|
3
|
+
from pymelos.commands.add import AddProjectCommand, AddProjectOptions, AddProjectResult, add_project
|
|
4
|
+
from pymelos.commands.base import Command, CommandContext, SyncCommand
|
|
5
|
+
from pymelos.commands.bootstrap import (
|
|
6
|
+
BootstrapCommand,
|
|
7
|
+
BootstrapOptions,
|
|
8
|
+
BootstrapResult,
|
|
9
|
+
bootstrap,
|
|
10
|
+
)
|
|
11
|
+
from pymelos.commands.changed import (
|
|
12
|
+
ChangedCommand,
|
|
13
|
+
ChangedOptions,
|
|
14
|
+
ChangedPackage,
|
|
15
|
+
ChangedResult,
|
|
16
|
+
get_changed_packages,
|
|
17
|
+
)
|
|
18
|
+
from pymelos.commands.clean import CleanCommand, CleanOptions, CleanResult, clean
|
|
19
|
+
from pymelos.commands.exec import ExecCommand, ExecOptions, exec_command
|
|
20
|
+
from pymelos.commands.list import (
|
|
21
|
+
ListCommand,
|
|
22
|
+
ListFormat,
|
|
23
|
+
ListOptions,
|
|
24
|
+
ListResult,
|
|
25
|
+
PackageInfo,
|
|
26
|
+
list_packages,
|
|
27
|
+
)
|
|
28
|
+
from pymelos.commands.release import (
|
|
29
|
+
PackageRelease,
|
|
30
|
+
ReleaseCommand,
|
|
31
|
+
ReleaseOptions,
|
|
32
|
+
ReleaseResult,
|
|
33
|
+
release,
|
|
34
|
+
)
|
|
35
|
+
from pymelos.commands.run import RunCommand, RunOptions, run_script
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Base
|
|
39
|
+
"Command",
|
|
40
|
+
"SyncCommand",
|
|
41
|
+
"CommandContext",
|
|
42
|
+
# Bootstrap
|
|
43
|
+
"BootstrapCommand",
|
|
44
|
+
"BootstrapOptions",
|
|
45
|
+
"BootstrapResult",
|
|
46
|
+
"bootstrap",
|
|
47
|
+
# Run
|
|
48
|
+
"RunCommand",
|
|
49
|
+
"RunOptions",
|
|
50
|
+
"run_script",
|
|
51
|
+
# Exec
|
|
52
|
+
"ExecCommand",
|
|
53
|
+
"ExecOptions",
|
|
54
|
+
"exec_command",
|
|
55
|
+
# List
|
|
56
|
+
"ListCommand",
|
|
57
|
+
"ListOptions",
|
|
58
|
+
"ListResult",
|
|
59
|
+
"ListFormat",
|
|
60
|
+
"PackageInfo",
|
|
61
|
+
"list_packages",
|
|
62
|
+
# Clean
|
|
63
|
+
"CleanCommand",
|
|
64
|
+
"CleanOptions",
|
|
65
|
+
"CleanResult",
|
|
66
|
+
"clean",
|
|
67
|
+
# Changed
|
|
68
|
+
"ChangedCommand",
|
|
69
|
+
"ChangedOptions",
|
|
70
|
+
"ChangedResult",
|
|
71
|
+
"ChangedPackage",
|
|
72
|
+
"get_changed_packages",
|
|
73
|
+
# Release
|
|
74
|
+
"ReleaseCommand",
|
|
75
|
+
"ReleaseOptions",
|
|
76
|
+
"ReleaseResult",
|
|
77
|
+
"PackageRelease",
|
|
78
|
+
"release",
|
|
79
|
+
# Add Project
|
|
80
|
+
"AddProjectCommand",
|
|
81
|
+
"AddProjectOptions",
|
|
82
|
+
"AddProjectResult",
|
|
83
|
+
"add_project",
|
|
84
|
+
]
|
pymelos/commands/add.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pymelos import Workspace
|
|
8
|
+
from pymelos.commands.base import Command, CommandContext, pip_install_editable
|
|
9
|
+
from pymelos.execution import run_command
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class AddProjectOptions:
|
|
14
|
+
name: str
|
|
15
|
+
project_type: Literal["lib", "app"] = "lib"
|
|
16
|
+
folder: str | None = None # default to workspace default folder
|
|
17
|
+
bare: bool = False
|
|
18
|
+
editable: bool = True # whether to pip install -e after creation
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AddProjectResult:
|
|
22
|
+
"""Result of AddProjectCommand."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, success: bool, path: Path, message: str):
|
|
25
|
+
self.success = success
|
|
26
|
+
self.path = path
|
|
27
|
+
self.message = message
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AddProjectCommand(Command[AddProjectResult]):
|
|
31
|
+
"""Add a new library or app to the workspace."""
|
|
32
|
+
|
|
33
|
+
DEFAULT_FOLDERS = {
|
|
34
|
+
"lib": "packages",
|
|
35
|
+
"app": "examples",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def __init__(self, context: CommandContext, options: AddProjectOptions) -> None:
|
|
39
|
+
super().__init__(context)
|
|
40
|
+
self.options = options
|
|
41
|
+
|
|
42
|
+
async def execute(self) -> AddProjectResult:
|
|
43
|
+
name = self.options.name
|
|
44
|
+
project_type = self.options.project_type
|
|
45
|
+
|
|
46
|
+
# determine target folder
|
|
47
|
+
folder = self.options.folder or self.DEFAULT_FOLDERS[project_type]
|
|
48
|
+
target_dir = self.workspace.root / folder
|
|
49
|
+
project_path = target_dir / name
|
|
50
|
+
|
|
51
|
+
if project_path.exists():
|
|
52
|
+
raise RuntimeError(f"Project {name} already exists at {project_path}")
|
|
53
|
+
|
|
54
|
+
# run command
|
|
55
|
+
exit_code, stdout, stderr, _ = await run_command(
|
|
56
|
+
f"uv init {name} --{project_type} {'--bare' if self.options.bare else ''}",
|
|
57
|
+
cwd=target_dir,
|
|
58
|
+
env=self.context.env,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if project_type == "lib" and self.options.editable:
|
|
62
|
+
pip_install_editable(project_path)
|
|
63
|
+
# return result
|
|
64
|
+
return AddProjectResult(True, project_path, f"Created project {name} at {project_path}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def add_project(
|
|
68
|
+
workspace: Workspace,
|
|
69
|
+
name: str,
|
|
70
|
+
project_type: Literal["lib", "app"] = "lib",
|
|
71
|
+
folder: str | None = None,
|
|
72
|
+
editable: bool = True,
|
|
73
|
+
) -> AddProjectResult:
|
|
74
|
+
context = CommandContext(workspace=workspace)
|
|
75
|
+
return await AddProjectCommand(
|
|
76
|
+
context, AddProjectOptions(name, project_type, folder, editable=editable)
|
|
77
|
+
).execute()
|
pymelos/commands/base.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Base command infrastructure."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from subprocess import run
|
|
10
|
+
from typing import Generic, TypeVar
|
|
11
|
+
|
|
12
|
+
from pymelos.workspace import Workspace
|
|
13
|
+
|
|
14
|
+
TResult = TypeVar("TResult")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class CommandContext:
|
|
19
|
+
"""Context passed to all commands.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
workspace: The workspace instance.
|
|
23
|
+
dry_run: If True, show what would happen without making changes.
|
|
24
|
+
verbose: If True, show detailed output.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
workspace: Workspace
|
|
28
|
+
dry_run: bool = False
|
|
29
|
+
verbose: bool = False
|
|
30
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Command(ABC, Generic[TResult]):
|
|
34
|
+
"""Base class for all pymelos commands.
|
|
35
|
+
|
|
36
|
+
Commands encapsulate the logic for a specific operation.
|
|
37
|
+
They receive a context and return a result.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, context: CommandContext) -> None:
|
|
41
|
+
"""Initialize command.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
context: Command context.
|
|
45
|
+
"""
|
|
46
|
+
self.context = context
|
|
47
|
+
self.workspace = context.workspace
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
async def execute(self) -> TResult:
|
|
51
|
+
"""Execute the command.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Command-specific result.
|
|
55
|
+
"""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
def validate(self) -> list[str]:
|
|
59
|
+
"""Validate that the command can be executed.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
List of validation errors (empty if valid).
|
|
63
|
+
"""
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SyncCommand(ABC, Generic[TResult]):
|
|
68
|
+
"""Base class for synchronous commands."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, context: CommandContext) -> None:
|
|
71
|
+
"""Initialize command.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
context: Command context.
|
|
75
|
+
"""
|
|
76
|
+
self.context = context
|
|
77
|
+
self.workspace = context.workspace
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def execute(self) -> TResult:
|
|
81
|
+
"""Execute the command synchronously.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Command-specific result.
|
|
85
|
+
"""
|
|
86
|
+
...
|
|
87
|
+
|
|
88
|
+
def validate(self) -> list[str]:
|
|
89
|
+
"""Validate that the command can be executed.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
List of validation errors (empty if valid).
|
|
93
|
+
"""
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def pip_install_editable(paths: Path | Sequence[Path | str]) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Install one or more workspace packages in editable mode.
|
|
100
|
+
"""
|
|
101
|
+
if isinstance(paths, Path | str):
|
|
102
|
+
paths = [paths] # wrap single path into list
|
|
103
|
+
|
|
104
|
+
# Convert all to strings
|
|
105
|
+
paths_str = [str(p) for p in paths]
|
|
106
|
+
|
|
107
|
+
# Run uv pip install -e for all packages
|
|
108
|
+
run(["uv", "pip", "install", "-e", *paths_str], check=True)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Bootstrap command implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from pymelos.commands.base import Command, CommandContext, pip_install_editable
|
|
8
|
+
from pymelos.execution import ExecutionResult
|
|
9
|
+
from pymelos.uv import sync
|
|
10
|
+
from pymelos.workspace.workspace import Workspace
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class BootstrapResult:
|
|
15
|
+
"""Result of bootstrap command.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
success: Whether bootstrap succeeded.
|
|
19
|
+
packages_installed: Number of packages in workspace.
|
|
20
|
+
hook_results: Results of bootstrap hooks.
|
|
21
|
+
uv_output: Output from uv sync.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
success: bool
|
|
25
|
+
packages_installed: int
|
|
26
|
+
hook_results: list[ExecutionResult]
|
|
27
|
+
uv_output: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class BootstrapOptions:
|
|
32
|
+
"""Options for bootstrap command."""
|
|
33
|
+
|
|
34
|
+
clean_first: bool = False
|
|
35
|
+
frozen: bool = False
|
|
36
|
+
locked: bool = True
|
|
37
|
+
skip_hooks: bool = False
|
|
38
|
+
editable: bool = True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BootstrapCommand(Command[BootstrapResult]):
|
|
42
|
+
"""Bootstrap the workspace.
|
|
43
|
+
|
|
44
|
+
This command:
|
|
45
|
+
1. Runs uv sync to install dependencies
|
|
46
|
+
2. Verifies workspace package linking
|
|
47
|
+
3. Runs bootstrap hooks from configuration
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, context: CommandContext, options: BootstrapOptions | None = None) -> None:
|
|
51
|
+
super().__init__(context)
|
|
52
|
+
self.options = options or BootstrapOptions()
|
|
53
|
+
|
|
54
|
+
async def execute(self) -> BootstrapResult:
|
|
55
|
+
"""Execute bootstrap."""
|
|
56
|
+
from pymelos.commands.clean import CleanCommand, CleanOptions
|
|
57
|
+
from pymelos.execution import run_in_package
|
|
58
|
+
|
|
59
|
+
hook_results: list[ExecutionResult] = []
|
|
60
|
+
|
|
61
|
+
# Optionally clean first
|
|
62
|
+
if self.options.clean_first:
|
|
63
|
+
clean_cmd = CleanCommand(self.context, CleanOptions())
|
|
64
|
+
await clean_cmd.execute()
|
|
65
|
+
|
|
66
|
+
# Check if lockfile exists - if not, don't use --locked flag
|
|
67
|
+
lockfile = self.workspace.root / "uv.lock"
|
|
68
|
+
use_locked = self.options.locked and lockfile.exists()
|
|
69
|
+
|
|
70
|
+
# Run uv sync
|
|
71
|
+
exit_code, stdout, stderr = sync(
|
|
72
|
+
self.workspace.root,
|
|
73
|
+
frozen=self.options.frozen,
|
|
74
|
+
locked=use_locked,
|
|
75
|
+
dev=True,
|
|
76
|
+
all_packages=True,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# If --locked failed due to outdated lockfile, retry without --locked
|
|
80
|
+
if exit_code != 0 and use_locked and "needs to be updated" in stderr:
|
|
81
|
+
exit_code, stdout, stderr = sync(
|
|
82
|
+
self.workspace.root,
|
|
83
|
+
frozen=self.options.frozen,
|
|
84
|
+
locked=False,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if exit_code != 0:
|
|
88
|
+
return BootstrapResult(
|
|
89
|
+
success=False,
|
|
90
|
+
packages_installed=0,
|
|
91
|
+
hook_results=[],
|
|
92
|
+
uv_output=stderr or stdout,
|
|
93
|
+
)
|
|
94
|
+
# Install workspace packages (editable)
|
|
95
|
+
if self.options.editable and self.workspace.packages:
|
|
96
|
+
package_paths = [pkg.path for pkg in self.workspace.packages.values()]
|
|
97
|
+
pip_install_editable(package_paths)
|
|
98
|
+
|
|
99
|
+
# Run bootstrap hooks
|
|
100
|
+
if not self.options.skip_hooks:
|
|
101
|
+
for hook in self.workspace.config.bootstrap.hooks:
|
|
102
|
+
if hook.run_once:
|
|
103
|
+
# Run at workspace root
|
|
104
|
+
from pymelos.execution.runner import run_command
|
|
105
|
+
|
|
106
|
+
_, out, err, _ = await run_command(
|
|
107
|
+
hook.run,
|
|
108
|
+
self.workspace.root,
|
|
109
|
+
env=self.context.env,
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
# Run in matching packages
|
|
113
|
+
packages = self.workspace.filter_packages(scope=hook.scope)
|
|
114
|
+
for pkg in packages:
|
|
115
|
+
result = await run_in_package(pkg, hook.run, env=self.context.env)
|
|
116
|
+
hook_results.append(result)
|
|
117
|
+
|
|
118
|
+
return BootstrapResult(
|
|
119
|
+
success=True,
|
|
120
|
+
packages_installed=len(self.workspace.packages),
|
|
121
|
+
hook_results=hook_results,
|
|
122
|
+
uv_output=stdout,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def bootstrap(
|
|
127
|
+
workspace: Workspace,
|
|
128
|
+
*,
|
|
129
|
+
clean_first: bool = False,
|
|
130
|
+
frozen: bool = False,
|
|
131
|
+
skip_hooks: bool = False,
|
|
132
|
+
verbose: bool = False,
|
|
133
|
+
) -> BootstrapResult:
|
|
134
|
+
"""Convenience function to bootstrap a workspace.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
workspace: Workspace to bootstrap.
|
|
138
|
+
clean_first: Clean before bootstrap.
|
|
139
|
+
frozen: Use frozen dependencies.
|
|
140
|
+
skip_hooks: Skip bootstrap hooks.
|
|
141
|
+
verbose: Show verbose output.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Bootstrap result.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
context = CommandContext(workspace=workspace, verbose=verbose)
|
|
148
|
+
options = BootstrapOptions(
|
|
149
|
+
clean_first=clean_first,
|
|
150
|
+
frozen=frozen,
|
|
151
|
+
skip_hooks=skip_hooks,
|
|
152
|
+
)
|
|
153
|
+
cmd = BootstrapCommand(context, options)
|
|
154
|
+
return await cmd.execute()
|