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,161 @@
|
|
|
1
|
+
"""Changed command implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from pymelos.commands.base import CommandContext, SyncCommand
|
|
9
|
+
from pymelos.workspace.workspace import Workspace
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ChangedPackage:
|
|
17
|
+
"""Information about a changed package."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
path: str
|
|
21
|
+
files_changed: int
|
|
22
|
+
is_dependent: bool # True if changed due to dependency
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ChangedResult:
|
|
27
|
+
"""Result of changed command."""
|
|
28
|
+
|
|
29
|
+
since: str
|
|
30
|
+
changed: list[ChangedPackage]
|
|
31
|
+
total_files_changed: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ChangedOptions:
|
|
36
|
+
"""Options for changed command."""
|
|
37
|
+
|
|
38
|
+
since: str
|
|
39
|
+
include_dependents: bool = True
|
|
40
|
+
scope: str | None = None
|
|
41
|
+
ignore: list[str] | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ChangedCommand(SyncCommand[ChangedResult]):
|
|
45
|
+
"""List packages that have changed since a git reference."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, context: CommandContext, options: ChangedOptions) -> None:
|
|
48
|
+
super().__init__(context)
|
|
49
|
+
self.options = options
|
|
50
|
+
|
|
51
|
+
def execute(self) -> ChangedResult:
|
|
52
|
+
"""Execute the changed command."""
|
|
53
|
+
from pymelos.filters import apply_filters
|
|
54
|
+
from pymelos.git import get_changed_files_since
|
|
55
|
+
|
|
56
|
+
# Get all changed files
|
|
57
|
+
changed_files = get_changed_files_since(self.workspace.root, self.options.since)
|
|
58
|
+
|
|
59
|
+
# Map files to packages
|
|
60
|
+
directly_changed: dict[str, list[str]] = {} # package -> files
|
|
61
|
+
|
|
62
|
+
for pkg in self.workspace.packages.values():
|
|
63
|
+
pkg_files: list[str] = []
|
|
64
|
+
for file_path in changed_files:
|
|
65
|
+
abs_path = self.workspace.root / file_path
|
|
66
|
+
try:
|
|
67
|
+
abs_path.relative_to(pkg.path)
|
|
68
|
+
pkg_files.append(str(file_path))
|
|
69
|
+
except ValueError:
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
if pkg_files:
|
|
73
|
+
directly_changed[pkg.name] = pkg_files
|
|
74
|
+
|
|
75
|
+
# Get dependents if requested
|
|
76
|
+
dependent_packages: set[str] = set()
|
|
77
|
+
if self.options.include_dependents:
|
|
78
|
+
for pkg_name in list(directly_changed.keys()):
|
|
79
|
+
dependents = self.workspace.graph.get_transitive_dependents(pkg_name)
|
|
80
|
+
for dep in dependents:
|
|
81
|
+
if dep.name not in directly_changed:
|
|
82
|
+
dependent_packages.add(dep.name)
|
|
83
|
+
|
|
84
|
+
# Build result
|
|
85
|
+
changed_pkgs: list[ChangedPackage] = []
|
|
86
|
+
|
|
87
|
+
# Add directly changed packages
|
|
88
|
+
for pkg_name, files in directly_changed.items():
|
|
89
|
+
pkg = self.workspace.get_package(pkg_name)
|
|
90
|
+
changed_pkgs.append(
|
|
91
|
+
ChangedPackage(
|
|
92
|
+
name=pkg_name,
|
|
93
|
+
path=str(pkg.path.relative_to(self.workspace.root)),
|
|
94
|
+
files_changed=len(files),
|
|
95
|
+
is_dependent=False,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Add dependent packages
|
|
100
|
+
for pkg_name in dependent_packages:
|
|
101
|
+
pkg = self.workspace.get_package(pkg_name)
|
|
102
|
+
changed_pkgs.append(
|
|
103
|
+
ChangedPackage(
|
|
104
|
+
name=pkg_name,
|
|
105
|
+
path=str(pkg.path.relative_to(self.workspace.root)),
|
|
106
|
+
files_changed=0,
|
|
107
|
+
is_dependent=True,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Apply filters
|
|
112
|
+
if self.options.scope or self.options.ignore:
|
|
113
|
+
pkg_list = [self.workspace.get_package(p.name) for p in changed_pkgs]
|
|
114
|
+
filtered = apply_filters(
|
|
115
|
+
pkg_list,
|
|
116
|
+
scope=self.options.scope,
|
|
117
|
+
ignore=self.options.ignore,
|
|
118
|
+
)
|
|
119
|
+
filtered_names = {p.name for p in filtered}
|
|
120
|
+
changed_pkgs = [p for p in changed_pkgs if p.name in filtered_names]
|
|
121
|
+
|
|
122
|
+
# Sort by name
|
|
123
|
+
changed_pkgs.sort(key=lambda p: p.name)
|
|
124
|
+
|
|
125
|
+
return ChangedResult(
|
|
126
|
+
since=self.options.since,
|
|
127
|
+
changed=changed_pkgs,
|
|
128
|
+
total_files_changed=len(changed_files),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_changed_packages(
|
|
133
|
+
workspace: Workspace,
|
|
134
|
+
since: str,
|
|
135
|
+
*,
|
|
136
|
+
include_dependents: bool = True,
|
|
137
|
+
scope: str | None = None,
|
|
138
|
+
ignore: list[str] | None = None,
|
|
139
|
+
) -> ChangedResult:
|
|
140
|
+
"""Convenience function to get changed packages.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
workspace: Workspace to check.
|
|
144
|
+
since: Git reference.
|
|
145
|
+
include_dependents: Include transitive dependents.
|
|
146
|
+
scope: Package scope filter.
|
|
147
|
+
ignore: Patterns to exclude.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Changed result.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
context = CommandContext(workspace=workspace)
|
|
154
|
+
options = ChangedOptions(
|
|
155
|
+
since=since,
|
|
156
|
+
include_dependents=include_dependents,
|
|
157
|
+
scope=scope,
|
|
158
|
+
ignore=ignore,
|
|
159
|
+
)
|
|
160
|
+
cmd = ChangedCommand(context, options)
|
|
161
|
+
return cmd.execute()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Clean command implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from pymelos.commands.base import Command, CommandContext
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pymelos.workspace import Package
|
|
14
|
+
from pymelos.workspace.workspace import Workspace
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class CleanResult:
|
|
19
|
+
"""Result of clean command."""
|
|
20
|
+
|
|
21
|
+
files_removed: int
|
|
22
|
+
dirs_removed: int
|
|
23
|
+
bytes_freed: int
|
|
24
|
+
packages_cleaned: list[str]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CleanOptions:
|
|
29
|
+
"""Options for clean command."""
|
|
30
|
+
|
|
31
|
+
scope: str | None = None
|
|
32
|
+
patterns: list[str] | None = None
|
|
33
|
+
protected: list[str] | None = None
|
|
34
|
+
dry_run: bool = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CleanCommand(Command[CleanResult]):
|
|
38
|
+
"""Clean build artifacts from packages."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, context: CommandContext, options: CleanOptions | None = None) -> None:
|
|
41
|
+
super().__init__(context)
|
|
42
|
+
self.options = options or CleanOptions()
|
|
43
|
+
|
|
44
|
+
def get_packages(self) -> list[Package]:
|
|
45
|
+
"""Get packages to clean."""
|
|
46
|
+
from pymelos.filters import apply_filters
|
|
47
|
+
|
|
48
|
+
packages = list(self.workspace.packages.values())
|
|
49
|
+
return apply_filters(packages, scope=self.options.scope)
|
|
50
|
+
|
|
51
|
+
def get_patterns(self) -> list[str]:
|
|
52
|
+
"""Get patterns to clean."""
|
|
53
|
+
if self.options.patterns:
|
|
54
|
+
return self.options.patterns
|
|
55
|
+
return self.workspace.config.clean.patterns
|
|
56
|
+
|
|
57
|
+
def get_protected(self) -> set[str]:
|
|
58
|
+
"""Get protected patterns."""
|
|
59
|
+
if self.options.protected:
|
|
60
|
+
return set(self.options.protected)
|
|
61
|
+
return set(self.workspace.config.clean.protected)
|
|
62
|
+
|
|
63
|
+
def is_protected(self, path: Path, protected: set[str]) -> bool:
|
|
64
|
+
"""Check if a path is protected."""
|
|
65
|
+
import fnmatch
|
|
66
|
+
|
|
67
|
+
return any(fnmatch.fnmatch(path.name, pattern) for pattern in protected)
|
|
68
|
+
|
|
69
|
+
def _calculate_size(self, path: Path) -> int:
|
|
70
|
+
"""Calculate total size of a path (file or directory)."""
|
|
71
|
+
if path.is_file():
|
|
72
|
+
return path.stat().st_size
|
|
73
|
+
return sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
|
|
74
|
+
|
|
75
|
+
def _get_paths_to_clean(
|
|
76
|
+
self, packages: list[Package], patterns: list[str], protected: set[str]
|
|
77
|
+
) -> list[tuple[str, Path]]:
|
|
78
|
+
"""Get all paths to clean with their package names."""
|
|
79
|
+
return [
|
|
80
|
+
(pkg.name, path)
|
|
81
|
+
for pkg in packages
|
|
82
|
+
for pattern in patterns
|
|
83
|
+
for path in pkg.path.glob(pattern)
|
|
84
|
+
if not self.is_protected(path, protected)
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
async def execute(self) -> CleanResult:
|
|
88
|
+
"""Execute the clean command."""
|
|
89
|
+
paths_to_clean = self._get_paths_to_clean(
|
|
90
|
+
self.get_packages(), self.get_patterns(), self.get_protected()
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
files_removed = 0
|
|
94
|
+
dirs_removed = 0
|
|
95
|
+
bytes_freed = 0
|
|
96
|
+
packages_cleaned: set[str] = set()
|
|
97
|
+
is_dry_run = self.options.dry_run or self.context.dry_run
|
|
98
|
+
|
|
99
|
+
for pkg_name, path in paths_to_clean:
|
|
100
|
+
bytes_freed += self._calculate_size(path)
|
|
101
|
+
packages_cleaned.add(pkg_name)
|
|
102
|
+
|
|
103
|
+
if path.is_file():
|
|
104
|
+
files_removed += 1
|
|
105
|
+
if not is_dry_run:
|
|
106
|
+
path.unlink()
|
|
107
|
+
elif path.is_dir():
|
|
108
|
+
dirs_removed += 1
|
|
109
|
+
if not is_dry_run:
|
|
110
|
+
shutil.rmtree(path)
|
|
111
|
+
|
|
112
|
+
return CleanResult(
|
|
113
|
+
files_removed=files_removed,
|
|
114
|
+
dirs_removed=dirs_removed,
|
|
115
|
+
bytes_freed=bytes_freed,
|
|
116
|
+
packages_cleaned=list(packages_cleaned),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def clean(
|
|
121
|
+
workspace: Workspace,
|
|
122
|
+
*,
|
|
123
|
+
scope: str | None = None,
|
|
124
|
+
patterns: list[str] | None = None,
|
|
125
|
+
dry_run: bool = False,
|
|
126
|
+
) -> CleanResult:
|
|
127
|
+
"""Convenience function to clean packages.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
workspace: Workspace to clean.
|
|
131
|
+
scope: Package scope filter.
|
|
132
|
+
patterns: Override clean patterns.
|
|
133
|
+
dry_run: Show what would be cleaned.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Clean result.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
context = CommandContext(workspace=workspace, dry_run=dry_run)
|
|
140
|
+
options = CleanOptions(scope=scope, patterns=patterns, dry_run=dry_run)
|
|
141
|
+
cmd = CleanCommand(context, options)
|
|
142
|
+
return await cmd.execute()
|
pymelos/commands/exec.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Exec command implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from pymelos.commands.base import Command, CommandContext
|
|
9
|
+
from pymelos.execution import BatchResult, ParallelExecutor
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pymelos.workspace import Package
|
|
13
|
+
from pymelos.workspace.workspace import Workspace
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ExecOptions:
|
|
18
|
+
"""Options for exec command."""
|
|
19
|
+
|
|
20
|
+
command: str
|
|
21
|
+
scope: str | None = None
|
|
22
|
+
since: str | None = None
|
|
23
|
+
ignore: list[str] | None = None
|
|
24
|
+
concurrency: int = 4
|
|
25
|
+
fail_fast: bool = False
|
|
26
|
+
topological: bool = False # exec doesn't default to topological
|
|
27
|
+
include_dependents: bool = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ExecCommand(Command[BatchResult]):
|
|
31
|
+
"""Execute an arbitrary command across packages.
|
|
32
|
+
|
|
33
|
+
Unlike 'run', exec takes a direct command string rather
|
|
34
|
+
than a script name from configuration.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, context: CommandContext, options: ExecOptions) -> None:
|
|
38
|
+
super().__init__(context)
|
|
39
|
+
self.options = options
|
|
40
|
+
|
|
41
|
+
def get_packages(self) -> list[Package]:
|
|
42
|
+
"""Get packages to execute command in."""
|
|
43
|
+
from pymelos.filters import apply_filters_with_since
|
|
44
|
+
|
|
45
|
+
packages = list(self.workspace.packages.values())
|
|
46
|
+
|
|
47
|
+
return apply_filters_with_since(
|
|
48
|
+
packages,
|
|
49
|
+
self.workspace,
|
|
50
|
+
scope=self.options.scope,
|
|
51
|
+
since=self.options.since,
|
|
52
|
+
ignore=self.options.ignore,
|
|
53
|
+
include_dependents=self.options.include_dependents,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def execute(self) -> BatchResult:
|
|
57
|
+
"""Execute the command."""
|
|
58
|
+
packages = self.get_packages()
|
|
59
|
+
if not packages:
|
|
60
|
+
return BatchResult(results=[])
|
|
61
|
+
|
|
62
|
+
# Build environment
|
|
63
|
+
env = dict(self.context.env)
|
|
64
|
+
env.update(self.workspace.config.env)
|
|
65
|
+
|
|
66
|
+
executor = ParallelExecutor(
|
|
67
|
+
concurrency=self.options.concurrency,
|
|
68
|
+
fail_fast=self.options.fail_fast,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if self.options.topological:
|
|
72
|
+
batches = self.workspace.parallel_batches(packages)
|
|
73
|
+
return await executor.execute_batches(batches, self.options.command, env=env)
|
|
74
|
+
else:
|
|
75
|
+
return await executor.execute(packages, self.options.command, env=env)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def exec_command(
|
|
79
|
+
workspace: Workspace,
|
|
80
|
+
command: str,
|
|
81
|
+
*,
|
|
82
|
+
scope: str | None = None,
|
|
83
|
+
since: str | None = None,
|
|
84
|
+
ignore: list[str] | None = None,
|
|
85
|
+
concurrency: int = 4,
|
|
86
|
+
fail_fast: bool = False,
|
|
87
|
+
topological: bool = False,
|
|
88
|
+
) -> BatchResult:
|
|
89
|
+
"""Convenience function to execute a command.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
workspace: Workspace to run in.
|
|
93
|
+
command: Command to execute.
|
|
94
|
+
scope: Package scope filter.
|
|
95
|
+
since: Git reference.
|
|
96
|
+
ignore: Patterns to exclude.
|
|
97
|
+
concurrency: Parallel jobs.
|
|
98
|
+
fail_fast: Stop on first failure.
|
|
99
|
+
topological: Respect dependency order.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Batch result.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
context = CommandContext(workspace=workspace)
|
|
106
|
+
options = ExecOptions(
|
|
107
|
+
command=command,
|
|
108
|
+
scope=scope,
|
|
109
|
+
since=since,
|
|
110
|
+
ignore=ignore,
|
|
111
|
+
concurrency=concurrency,
|
|
112
|
+
fail_fast=fail_fast,
|
|
113
|
+
topological=topological,
|
|
114
|
+
)
|
|
115
|
+
cmd = ExecCommand(context, options)
|
|
116
|
+
return await cmd.execute()
|
pymelos/commands/list.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""List command implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from pymelos.commands.base import CommandContext, SyncCommand
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pymelos.workspace import Package
|
|
13
|
+
from pymelos.workspace.workspace import Workspace
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ListFormat(Enum):
|
|
17
|
+
"""Output format for list command."""
|
|
18
|
+
|
|
19
|
+
TABLE = "table"
|
|
20
|
+
JSON = "json"
|
|
21
|
+
GRAPH = "graph"
|
|
22
|
+
NAMES = "names"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class PackageInfo:
|
|
27
|
+
"""Information about a package for display."""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
version: str
|
|
31
|
+
path: str
|
|
32
|
+
description: str | None
|
|
33
|
+
dependencies: list[str]
|
|
34
|
+
dependents: list[str]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ListResult:
|
|
39
|
+
"""Result of list command."""
|
|
40
|
+
|
|
41
|
+
packages: list[PackageInfo]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ListOptions:
|
|
46
|
+
"""Options for list command."""
|
|
47
|
+
|
|
48
|
+
scope: str | None = None
|
|
49
|
+
since: str | None = None
|
|
50
|
+
ignore: list[str] | None = None
|
|
51
|
+
format: ListFormat = ListFormat.TABLE
|
|
52
|
+
include_dependents: bool = False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ListCommand(SyncCommand[ListResult]):
|
|
56
|
+
"""List packages in the workspace."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, context: CommandContext, options: ListOptions | None = None) -> None:
|
|
59
|
+
super().__init__(context)
|
|
60
|
+
self.options = options or ListOptions()
|
|
61
|
+
|
|
62
|
+
def get_packages(self) -> list[Package]:
|
|
63
|
+
"""Get packages to list."""
|
|
64
|
+
from pymelos.filters import apply_filters_with_since
|
|
65
|
+
|
|
66
|
+
packages = list(self.workspace.packages.values())
|
|
67
|
+
|
|
68
|
+
return apply_filters_with_since(
|
|
69
|
+
packages,
|
|
70
|
+
self.workspace,
|
|
71
|
+
scope=self.options.scope,
|
|
72
|
+
since=self.options.since,
|
|
73
|
+
ignore=self.options.ignore,
|
|
74
|
+
include_dependents=self.options.include_dependents,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def execute(self) -> ListResult:
|
|
78
|
+
"""Execute the list command."""
|
|
79
|
+
packages = self.get_packages()
|
|
80
|
+
graph = self.workspace.graph
|
|
81
|
+
|
|
82
|
+
infos: list[PackageInfo] = []
|
|
83
|
+
for pkg in packages:
|
|
84
|
+
deps = graph.get_dependencies(pkg.name)
|
|
85
|
+
dependents = graph.get_dependents(pkg.name)
|
|
86
|
+
|
|
87
|
+
infos.append(
|
|
88
|
+
PackageInfo(
|
|
89
|
+
name=pkg.name,
|
|
90
|
+
version=pkg.version,
|
|
91
|
+
path=str(pkg.path.relative_to(self.workspace.root)),
|
|
92
|
+
description=pkg.description,
|
|
93
|
+
dependencies=[d.name for d in deps],
|
|
94
|
+
dependents=[d.name for d in dependents],
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Sort by name
|
|
99
|
+
infos.sort(key=lambda p: p.name)
|
|
100
|
+
|
|
101
|
+
return ListResult(packages=infos)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def list_packages(
|
|
105
|
+
workspace: Workspace,
|
|
106
|
+
*,
|
|
107
|
+
scope: str | None = None,
|
|
108
|
+
since: str | None = None,
|
|
109
|
+
ignore: list[str] | None = None,
|
|
110
|
+
format: ListFormat = ListFormat.TABLE,
|
|
111
|
+
) -> ListResult:
|
|
112
|
+
"""Convenience function to list packages.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
workspace: Workspace to list.
|
|
116
|
+
scope: Package scope filter.
|
|
117
|
+
since: Git reference.
|
|
118
|
+
ignore: Patterns to exclude.
|
|
119
|
+
format: Output format.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
List result with package info.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
context = CommandContext(workspace=workspace)
|
|
126
|
+
options = ListOptions(scope=scope, since=since, ignore=ignore, format=format)
|
|
127
|
+
cmd = ListCommand(context, options)
|
|
128
|
+
return cmd.execute()
|