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,172 @@
|
|
|
1
|
+
"""Execution result models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ExecutionStatus(Enum):
|
|
10
|
+
"""Status of a command execution."""
|
|
11
|
+
|
|
12
|
+
SUCCESS = "success"
|
|
13
|
+
FAILURE = "failure"
|
|
14
|
+
SKIPPED = "skipped"
|
|
15
|
+
CANCELLED = "cancelled"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class ExecutionResult:
|
|
20
|
+
"""Result of executing a command on a package.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
package_name: Name of the package the command ran in.
|
|
24
|
+
status: Execution status.
|
|
25
|
+
exit_code: Process exit code.
|
|
26
|
+
stdout: Standard output.
|
|
27
|
+
stderr: Standard error.
|
|
28
|
+
duration_ms: Execution duration in milliseconds.
|
|
29
|
+
error: Exception if any occurred.
|
|
30
|
+
command: The command that was executed.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
package_name: str
|
|
34
|
+
status: ExecutionStatus
|
|
35
|
+
exit_code: int
|
|
36
|
+
stdout: str = ""
|
|
37
|
+
stderr: str = ""
|
|
38
|
+
duration_ms: int = 0
|
|
39
|
+
error: Exception | None = None
|
|
40
|
+
command: str = ""
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def success(self) -> bool:
|
|
44
|
+
"""Check if execution was successful."""
|
|
45
|
+
return self.status == ExecutionStatus.SUCCESS
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def failed(self) -> bool:
|
|
49
|
+
"""Check if execution failed."""
|
|
50
|
+
return self.status == ExecutionStatus.FAILURE
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def skipped(self) -> bool:
|
|
54
|
+
"""Check if execution was skipped."""
|
|
55
|
+
return self.status == ExecutionStatus.SKIPPED
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def success_result(
|
|
59
|
+
cls,
|
|
60
|
+
package_name: str,
|
|
61
|
+
stdout: str = "",
|
|
62
|
+
stderr: str = "",
|
|
63
|
+
duration_ms: int = 0,
|
|
64
|
+
command: str = "",
|
|
65
|
+
) -> ExecutionResult:
|
|
66
|
+
"""Create a success result."""
|
|
67
|
+
return cls(
|
|
68
|
+
package_name=package_name,
|
|
69
|
+
status=ExecutionStatus.SUCCESS,
|
|
70
|
+
exit_code=0,
|
|
71
|
+
stdout=stdout,
|
|
72
|
+
stderr=stderr,
|
|
73
|
+
duration_ms=duration_ms,
|
|
74
|
+
command=command,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def failure_result(
|
|
79
|
+
cls,
|
|
80
|
+
package_name: str,
|
|
81
|
+
exit_code: int,
|
|
82
|
+
stdout: str = "",
|
|
83
|
+
stderr: str = "",
|
|
84
|
+
duration_ms: int = 0,
|
|
85
|
+
error: Exception | None = None,
|
|
86
|
+
command: str = "",
|
|
87
|
+
) -> ExecutionResult:
|
|
88
|
+
"""Create a failure result."""
|
|
89
|
+
return cls(
|
|
90
|
+
package_name=package_name,
|
|
91
|
+
status=ExecutionStatus.FAILURE,
|
|
92
|
+
exit_code=exit_code,
|
|
93
|
+
stdout=stdout,
|
|
94
|
+
stderr=stderr,
|
|
95
|
+
duration_ms=duration_ms,
|
|
96
|
+
error=error,
|
|
97
|
+
command=command,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def skipped_result(
|
|
102
|
+
cls,
|
|
103
|
+
package_name: str,
|
|
104
|
+
reason: str = "",
|
|
105
|
+
) -> ExecutionResult:
|
|
106
|
+
"""Create a skipped result."""
|
|
107
|
+
return cls(
|
|
108
|
+
package_name=package_name,
|
|
109
|
+
status=ExecutionStatus.SKIPPED,
|
|
110
|
+
exit_code=0,
|
|
111
|
+
stdout=reason,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class BatchResult:
|
|
117
|
+
"""Result of executing a command across multiple packages.
|
|
118
|
+
|
|
119
|
+
Attributes:
|
|
120
|
+
results: Individual execution results.
|
|
121
|
+
total_duration_ms: Total execution duration.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
results: list[ExecutionResult] = field(default_factory=list)
|
|
125
|
+
total_duration_ms: int = 0
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def all_success(self) -> bool:
|
|
129
|
+
"""Check if all executions were successful."""
|
|
130
|
+
return all(r.success or r.skipped for r in self.results)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def any_failure(self) -> bool:
|
|
134
|
+
"""Check if any execution failed."""
|
|
135
|
+
return any(r.failed for r in self.results)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def success_count(self) -> int:
|
|
139
|
+
"""Count of successful executions."""
|
|
140
|
+
return sum(1 for r in self.results if r.success)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def failure_count(self) -> int:
|
|
144
|
+
"""Count of failed executions."""
|
|
145
|
+
return sum(1 for r in self.results if r.failed)
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def skipped_count(self) -> int:
|
|
149
|
+
"""Count of skipped executions."""
|
|
150
|
+
return sum(1 for r in self.results if r.skipped)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def failed_packages(self) -> list[str]:
|
|
154
|
+
"""Get names of packages that failed."""
|
|
155
|
+
return [r.package_name for r in self.results if r.failed]
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def successful_packages(self) -> list[str]:
|
|
159
|
+
"""Get names of packages that succeeded."""
|
|
160
|
+
return [r.package_name for r in self.results if r.success]
|
|
161
|
+
|
|
162
|
+
def add(self, result: ExecutionResult) -> None:
|
|
163
|
+
"""Add a result to the batch."""
|
|
164
|
+
self.results.append(result)
|
|
165
|
+
|
|
166
|
+
def __len__(self) -> int:
|
|
167
|
+
"""Number of results."""
|
|
168
|
+
return len(self.results)
|
|
169
|
+
|
|
170
|
+
def __iter__(self):
|
|
171
|
+
"""Iterate over results."""
|
|
172
|
+
return iter(self.results)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Command execution engine with standard asynchronous capture."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from pymelos.execution.results import ExecutionResult
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pymelos.workspace.package import Package
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def run_command(
|
|
18
|
+
command: str,
|
|
19
|
+
cwd: Path,
|
|
20
|
+
*,
|
|
21
|
+
env: dict[str, str] | None = None,
|
|
22
|
+
timeout: float | None = None,
|
|
23
|
+
) -> tuple[int, str, str, int]:
|
|
24
|
+
"""Run a shell command asynchronously.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
command: Shell command to execute.
|
|
28
|
+
cwd: Working directory.
|
|
29
|
+
env: Environment variables (merged with current env).
|
|
30
|
+
timeout: Timeout in seconds.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Tuple of (exit_code, stdout, stderr, duration_ms).
|
|
34
|
+
"""
|
|
35
|
+
# Merge environment
|
|
36
|
+
run_env = os.environ.copy()
|
|
37
|
+
if env:
|
|
38
|
+
run_env.update(env)
|
|
39
|
+
|
|
40
|
+
start_time = time.monotonic()
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
process = await asyncio.create_subprocess_shell(
|
|
44
|
+
command,
|
|
45
|
+
cwd=str(cwd),
|
|
46
|
+
stdout=asyncio.subprocess.PIPE,
|
|
47
|
+
stderr=asyncio.subprocess.PIPE,
|
|
48
|
+
env=run_env,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
# Use communicate to wait for the process and capture all output at once
|
|
53
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
54
|
+
process.communicate(),
|
|
55
|
+
timeout=timeout,
|
|
56
|
+
)
|
|
57
|
+
except (asyncio.TimeoutError, TimeoutError):
|
|
58
|
+
process.kill()
|
|
59
|
+
await process.wait()
|
|
60
|
+
duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
61
|
+
return -1, "", f"Command timed out after {timeout}s", duration_ms
|
|
62
|
+
|
|
63
|
+
duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
64
|
+
|
|
65
|
+
stdout = stdout_bytes.decode("utf-8", errors="replace")
|
|
66
|
+
stderr = stderr_bytes.decode("utf-8", errors="replace")
|
|
67
|
+
|
|
68
|
+
return process.returncode or 0, stdout, stderr, duration_ms
|
|
69
|
+
|
|
70
|
+
except Exception as e:
|
|
71
|
+
duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
72
|
+
return -1, "", str(e), duration_ms
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def run_in_package(
|
|
76
|
+
package: Package,
|
|
77
|
+
command: str,
|
|
78
|
+
*,
|
|
79
|
+
env: dict[str, str] | None = None,
|
|
80
|
+
timeout: float | None = None,
|
|
81
|
+
) -> ExecutionResult:
|
|
82
|
+
"""Run a command in a package directory.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
package: Package to run command in.
|
|
86
|
+
command: Shell command to execute.
|
|
87
|
+
env: Additional environment variables.
|
|
88
|
+
timeout: Timeout in seconds.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Execution result.
|
|
92
|
+
"""
|
|
93
|
+
print(package.name, command)
|
|
94
|
+
# Build environment with package-specific variables
|
|
95
|
+
run_env = env.copy() if env else {}
|
|
96
|
+
run_env["PYMELOS_PACKAGE_NAME"] = package.name
|
|
97
|
+
run_env["PYMELOS_PACKAGE_PATH"] = str(package.path)
|
|
98
|
+
run_env["PYMELOS_PACKAGE_VERSION"] = package.version
|
|
99
|
+
|
|
100
|
+
exit_code, stdout, stderr, duration_ms = await run_command(
|
|
101
|
+
command,
|
|
102
|
+
cwd=package.path,
|
|
103
|
+
env=run_env,
|
|
104
|
+
timeout=timeout,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if exit_code == 0:
|
|
108
|
+
return ExecutionResult.success_result(
|
|
109
|
+
package_name=package.name,
|
|
110
|
+
stdout=stdout,
|
|
111
|
+
stderr=stderr,
|
|
112
|
+
duration_ms=duration_ms,
|
|
113
|
+
command=command,
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
return ExecutionResult.failure_result(
|
|
117
|
+
package_name=package.name,
|
|
118
|
+
exit_code=exit_code,
|
|
119
|
+
stdout=stdout,
|
|
120
|
+
stderr=stderr,
|
|
121
|
+
duration_ms=duration_ms,
|
|
122
|
+
command=command,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def run_command_sync(
|
|
127
|
+
command: str,
|
|
128
|
+
cwd: Path,
|
|
129
|
+
*,
|
|
130
|
+
env: dict[str, str] | None = None,
|
|
131
|
+
timeout: float | None = None,
|
|
132
|
+
) -> tuple[int, str, str, int]:
|
|
133
|
+
"""Run a shell command synchronously.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
command: Shell command to execute.
|
|
137
|
+
cwd: Working directory.
|
|
138
|
+
env: Environment variables.
|
|
139
|
+
timeout: Timeout in seconds.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Tuple of (exit_code, stdout, stderr, duration_ms).
|
|
143
|
+
"""
|
|
144
|
+
import subprocess
|
|
145
|
+
|
|
146
|
+
run_env = os.environ.copy()
|
|
147
|
+
if env:
|
|
148
|
+
run_env.update(env)
|
|
149
|
+
|
|
150
|
+
start_time = time.monotonic()
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
result = subprocess.run(
|
|
154
|
+
command,
|
|
155
|
+
shell=True,
|
|
156
|
+
cwd=cwd,
|
|
157
|
+
capture_output=True,
|
|
158
|
+
text=True,
|
|
159
|
+
env=run_env,
|
|
160
|
+
timeout=timeout,
|
|
161
|
+
)
|
|
162
|
+
duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
163
|
+
return result.returncode, result.stdout, result.stderr, duration_ms
|
|
164
|
+
|
|
165
|
+
except subprocess.TimeoutExpired:
|
|
166
|
+
duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
167
|
+
return -1, "", f"Command timed out after {timeout}s", duration_ms
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
171
|
+
return -1, "", str(e), duration_ms
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Package filtering utilities."""
|
|
2
|
+
|
|
3
|
+
from pymelos.filters.chain import apply_filters, apply_filters_with_since
|
|
4
|
+
from pymelos.filters.ignore import filter_by_ignore, should_ignore
|
|
5
|
+
from pymelos.filters.scope import filter_by_scope, match_scope, parse_scope
|
|
6
|
+
from pymelos.filters.since import (
|
|
7
|
+
filter_by_since,
|
|
8
|
+
get_changed_files,
|
|
9
|
+
get_changed_packages,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
# Chain
|
|
14
|
+
"apply_filters",
|
|
15
|
+
"apply_filters_with_since",
|
|
16
|
+
# Scope
|
|
17
|
+
"filter_by_scope",
|
|
18
|
+
"match_scope",
|
|
19
|
+
"parse_scope",
|
|
20
|
+
# Ignore
|
|
21
|
+
"filter_by_ignore",
|
|
22
|
+
"should_ignore",
|
|
23
|
+
# Since
|
|
24
|
+
"filter_by_since",
|
|
25
|
+
"get_changed_files",
|
|
26
|
+
"get_changed_packages",
|
|
27
|
+
]
|
pymelos/filters/chain.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Filter chain composition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from pymelos.filters.ignore import filter_by_ignore
|
|
8
|
+
from pymelos.filters.scope import filter_by_scope
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pymelos.workspace.package import Package
|
|
12
|
+
from pymelos.workspace.workspace import Workspace
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def apply_filters(
|
|
16
|
+
packages: list[Package],
|
|
17
|
+
*,
|
|
18
|
+
scope: str | None = None,
|
|
19
|
+
ignore: list[str] | None = None,
|
|
20
|
+
names: list[str] | None = None,
|
|
21
|
+
) -> list[Package]:
|
|
22
|
+
"""Apply multiple filters to a package list.
|
|
23
|
+
|
|
24
|
+
Filters are applied in order:
|
|
25
|
+
1. Explicit names (if provided, only these packages)
|
|
26
|
+
2. Scope pattern matching
|
|
27
|
+
3. Ignore pattern exclusion
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
packages: List of packages to filter.
|
|
31
|
+
scope: Comma-separated names or glob patterns.
|
|
32
|
+
ignore: Patterns to exclude.
|
|
33
|
+
names: Explicit list of package names (overrides scope).
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Filtered list of packages.
|
|
37
|
+
"""
|
|
38
|
+
result = packages
|
|
39
|
+
|
|
40
|
+
# If explicit names provided, filter to just those first
|
|
41
|
+
if names:
|
|
42
|
+
name_set = set(names)
|
|
43
|
+
result = [p for p in result if p.name in name_set]
|
|
44
|
+
else:
|
|
45
|
+
# Apply scope filter
|
|
46
|
+
result = filter_by_scope(result, scope)
|
|
47
|
+
|
|
48
|
+
# Apply ignore filter
|
|
49
|
+
result = filter_by_ignore(result, ignore)
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def apply_filters_with_since(
|
|
55
|
+
packages: list[Package],
|
|
56
|
+
workspace: Workspace,
|
|
57
|
+
*,
|
|
58
|
+
scope: str | None = None,
|
|
59
|
+
since: str | None = None,
|
|
60
|
+
ignore: list[str] | None = None,
|
|
61
|
+
include_dependents: bool = False,
|
|
62
|
+
) -> list[Package]:
|
|
63
|
+
"""Apply filters including git-based since filter.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
packages: List of packages to filter.
|
|
67
|
+
workspace: Workspace instance (needed for since filter).
|
|
68
|
+
scope: Comma-separated names or glob patterns.
|
|
69
|
+
since: Git reference for change detection.
|
|
70
|
+
ignore: Patterns to exclude.
|
|
71
|
+
include_dependents: Include packages that depend on changed packages.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Filtered list of packages.
|
|
75
|
+
"""
|
|
76
|
+
from pymelos.filters.since import filter_by_since
|
|
77
|
+
|
|
78
|
+
result = packages
|
|
79
|
+
|
|
80
|
+
# Apply scope filter first
|
|
81
|
+
result = filter_by_scope(result, scope)
|
|
82
|
+
|
|
83
|
+
# Apply since filter (git-based)
|
|
84
|
+
result = filter_by_since(
|
|
85
|
+
result,
|
|
86
|
+
workspace,
|
|
87
|
+
since,
|
|
88
|
+
include_dependents=include_dependents,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Apply ignore filter last
|
|
92
|
+
result = filter_by_ignore(result, ignore)
|
|
93
|
+
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# Type alias for filter function signature
|
|
98
|
+
if TYPE_CHECKING:
|
|
99
|
+
from collections.abc import Callable
|
|
100
|
+
|
|
101
|
+
FilterFunc = Callable[[list["Package"]], list["Package"]]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Ignore-based package filtering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pymelos.workspace.package import Package
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def should_ignore(package: Package, patterns: list[str]) -> bool:
|
|
13
|
+
"""Check if a package matches any ignore pattern.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
package: Package to check.
|
|
17
|
+
patterns: List of ignore patterns.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
True if package should be ignored.
|
|
21
|
+
"""
|
|
22
|
+
if not patterns:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
name = package.name
|
|
26
|
+
path_str = str(package.path)
|
|
27
|
+
|
|
28
|
+
for pattern in patterns:
|
|
29
|
+
# Match by name
|
|
30
|
+
if fnmatch.fnmatch(name, pattern):
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
# Match by normalized name
|
|
34
|
+
if fnmatch.fnmatch(name.replace("-", "_"), pattern.replace("-", "_")):
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
# Match by path
|
|
38
|
+
if fnmatch.fnmatch(path_str, pattern):
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def filter_by_ignore(
|
|
45
|
+
packages: list[Package],
|
|
46
|
+
ignore: list[str] | None,
|
|
47
|
+
) -> list[Package]:
|
|
48
|
+
"""Filter out packages matching ignore patterns.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
packages: List of packages to filter.
|
|
52
|
+
ignore: List of ignore patterns.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Filtered list of packages (not matching any ignore pattern).
|
|
56
|
+
"""
|
|
57
|
+
if not ignore:
|
|
58
|
+
return packages
|
|
59
|
+
|
|
60
|
+
return [p for p in packages if not should_ignore(p, ignore)]
|
pymelos/filters/scope.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Scope-based package filtering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pymelos.workspace.package import Package
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_scope(scope: str) -> list[str]:
|
|
13
|
+
"""Parse a scope string into individual patterns.
|
|
14
|
+
|
|
15
|
+
Scope can be comma-separated names or glob patterns:
|
|
16
|
+
- "core,api" -> ["core", "api"]
|
|
17
|
+
- "*-lib" -> ["*-lib"]
|
|
18
|
+
- "core,*-lib" -> ["core", "*-lib"]
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
scope: Comma-separated scope string.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
List of individual patterns.
|
|
25
|
+
"""
|
|
26
|
+
if not scope:
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
patterns = [p.strip() for p in scope.split(",")]
|
|
30
|
+
return [p for p in patterns if p]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def match_scope(package: Package, patterns: list[str]) -> bool:
|
|
34
|
+
"""Check if a package matches any of the scope patterns.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
package: Package to check.
|
|
38
|
+
patterns: List of name or glob patterns.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if package matches any pattern.
|
|
42
|
+
"""
|
|
43
|
+
if not patterns:
|
|
44
|
+
return True # No filter means match all
|
|
45
|
+
|
|
46
|
+
name = package.name
|
|
47
|
+
|
|
48
|
+
for pattern in patterns:
|
|
49
|
+
# Exact match
|
|
50
|
+
if name == pattern:
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
# Case-insensitive exact match
|
|
54
|
+
if name.lower() == pattern.lower():
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
# Glob pattern match
|
|
58
|
+
if fnmatch.fnmatch(name, pattern):
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
# Try with normalized names (replace - with _)
|
|
62
|
+
normalized_name = name.replace("-", "_")
|
|
63
|
+
normalized_pattern = pattern.replace("-", "_")
|
|
64
|
+
if fnmatch.fnmatch(normalized_name, normalized_pattern):
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def filter_by_scope(
|
|
71
|
+
packages: list[Package],
|
|
72
|
+
scope: str | None,
|
|
73
|
+
) -> list[Package]:
|
|
74
|
+
"""Filter packages by scope pattern.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
packages: List of packages to filter.
|
|
78
|
+
scope: Comma-separated names or glob patterns.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Filtered list of packages.
|
|
82
|
+
"""
|
|
83
|
+
if not scope:
|
|
84
|
+
return packages
|
|
85
|
+
|
|
86
|
+
patterns = parse_scope(scope)
|
|
87
|
+
if not patterns:
|
|
88
|
+
return packages
|
|
89
|
+
|
|
90
|
+
return [p for p in packages if match_scope(p, patterns)]
|