wexample-wex-addon-dev-python 10.2.0__py3-none-any.whl → 11.1.0__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.
- wexample_wex_addon_dev_python/commands/code/check/mypy.py +4 -3
- wexample_wex_addon_dev_python/commands/code/check/pylint.py +25 -19
- wexample_wex_addon_dev_python/commands/code/check/pyright.py +18 -14
- wexample_wex_addon_dev_python/commands/code/check.py +4 -7
- wexample_wex_addon_dev_python/commands/code/format/black.py +14 -15
- wexample_wex_addon_dev_python/commands/code/format/isort.py +14 -15
- wexample_wex_addon_dev_python/commands/code/format.py +8 -5
- wexample_wex_addon_dev_python/commands/code/rename.py +134 -0
- wexample_wex_addon_dev_python/commands/examples/validate.py +1 -2
- wexample_wex_addon_dev_python/common/pypi_registry_gateway.py +5 -3
- wexample_wex_addon_dev_python/config_value/python_package_readme_config_value.py +2 -8
- wexample_wex_addon_dev_python/file/python_app_iml_file.py +3 -2
- wexample_wex_addon_dev_python/file/python_pyproject_toml_file.py +26 -13
- wexample_wex_addon_dev_python/helpers/pdm.py +3 -2
- wexample_wex_addon_dev_python/middleware/each_python_file_middleware.py +12 -14
- wexample_wex_addon_dev_python/refactor/__init__.py +7 -0
- wexample_wex_addon_dev_python/refactor/python_package_rename_handler.py +215 -0
- wexample_wex_addon_dev_python/refactor/symbol_kind.py +19 -0
- wexample_wex_addon_dev_python/selection/abstract_python_code_selection.py +22 -9
- wexample_wex_addon_dev_python/selection/python_code_performance_selection.py +8 -9
- wexample_wex_addon_dev_python/services/python/commands/service/install.py +7 -10
- wexample_wex_addon_dev_python/services/python/commands/service/setup.py +5 -3
- wexample_wex_addon_dev_python/workdir/mixin/with_profiling_python_workdir_mixin.py +15 -16
- wexample_wex_addon_dev_python/workdir/python_package_workdir.py +9 -7
- wexample_wex_addon_dev_python/workdir/python_packages_suite_workdir.py +2 -3
- wexample_wex_addon_dev_python/workdir/python_workdir.py +100 -20
- {wexample_wex_addon_dev_python-10.2.0.dist-info → wexample_wex_addon_dev_python-11.1.0.dist-info}/METADATA +10 -10
- {wexample_wex_addon_dev_python-10.2.0.dist-info → wexample_wex_addon_dev_python-11.1.0.dist-info}/RECORD +30 -26
- {wexample_wex_addon_dev_python-10.2.0.dist-info → wexample_wex_addon_dev_python-11.1.0.dist-info}/WHEEL +0 -0
- {wexample_wex_addon_dev_python-10.2.0.dist-info → wexample_wex_addon_dev_python-11.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from wexample_wex_core.middleware.each_file_middleware import EachFileMiddleware
|
|
@@ -32,12 +33,13 @@ class EachPythonFileMiddleware(EachFileMiddleware):
|
|
|
32
33
|
python_extension_only: bool = True
|
|
33
34
|
|
|
34
35
|
def __init__(self, **kwargs) -> None:
|
|
35
|
-
# Allow overriding the default settings
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
# Allow overriding the default settings (pop avoids a double-lookup)
|
|
37
|
+
self.python_extension_only = kwargs.pop(
|
|
38
|
+
"python_extension_only", self.python_extension_only
|
|
39
|
+
)
|
|
40
|
+
ignored = kwargs.pop("ignored_directories", None)
|
|
41
|
+
if ignored is not None:
|
|
42
|
+
self.ignored_directories = set(ignored)
|
|
41
43
|
|
|
42
44
|
super().__init__(**kwargs)
|
|
43
45
|
|
|
@@ -65,13 +67,9 @@ class EachPythonFileMiddleware(EachFileMiddleware):
|
|
|
65
67
|
Returns:
|
|
66
68
|
True if the item should be processed, False otherwise
|
|
67
69
|
"""
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
+
# Cheap string check before the expensive isfile() syscall: skip
|
|
71
|
+
# non-.py paths immediately when extension filtering is active.
|
|
72
|
+
if self.python_extension_only and not item_path.endswith(".py"):
|
|
70
73
|
return False
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
if self.python_extension_only:
|
|
74
|
-
return item_path.endswith(".py")
|
|
75
|
-
|
|
76
|
-
# Otherwise, accept all files
|
|
77
|
-
return True
|
|
75
|
+
return os.path.isfile(item_path)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import libcst as cst
|
|
7
|
+
|
|
8
|
+
# Directories never scanned for source files (build/venv artefacts, VCS).
|
|
9
|
+
_SKIP_DIR_NAMES: frozenset[str] = frozenset({
|
|
10
|
+
"__pycache__",
|
|
11
|
+
".venv",
|
|
12
|
+
"venv",
|
|
13
|
+
"env",
|
|
14
|
+
".git",
|
|
15
|
+
"node_modules",
|
|
16
|
+
".pytest_cache",
|
|
17
|
+
".mypy_cache",
|
|
18
|
+
".ruff_cache",
|
|
19
|
+
".pdm-build",
|
|
20
|
+
"dist",
|
|
21
|
+
"build",
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _dotted_to_parts(node) -> tuple[str, ...] | None:
|
|
26
|
+
"""Flatten a chained `cst.Attribute` / `cst.Name` into ('a', 'b', 'c')."""
|
|
27
|
+
parts: list[str] = []
|
|
28
|
+
cur = node
|
|
29
|
+
while isinstance(cur, cst.Attribute):
|
|
30
|
+
if not isinstance(cur.attr, cst.Name):
|
|
31
|
+
return None
|
|
32
|
+
parts.append(cur.attr.value)
|
|
33
|
+
cur = cur.value
|
|
34
|
+
if not isinstance(cur, cst.Name):
|
|
35
|
+
return None
|
|
36
|
+
parts.append(cur.value)
|
|
37
|
+
parts.reverse()
|
|
38
|
+
return tuple(parts)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parts_to_dotted(parts: tuple[str, ...]):
|
|
42
|
+
node: cst.BaseExpression = cst.Name(parts[0])
|
|
43
|
+
for part in parts[1:]:
|
|
44
|
+
node = cst.Attribute(value=node, attr=cst.Name(part))
|
|
45
|
+
return node
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class RenameReport:
|
|
50
|
+
"""Outcome of a package rename run.
|
|
51
|
+
|
|
52
|
+
`package_dir_old`/`new` describe the move (or proposed move in dry-run).
|
|
53
|
+
`files_changed` lists every .py whose imports were rewritten.
|
|
54
|
+
"""
|
|
55
|
+
files_changed: list[Path] = field(default_factory=list)
|
|
56
|
+
files_scanned: int = 0
|
|
57
|
+
package_dir_new: Path | None = None
|
|
58
|
+
package_dir_old: Path | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PythonPackageRenameHandler:
|
|
62
|
+
"""Rename a Python package (directory + its dotted name) within a workdir.
|
|
63
|
+
|
|
64
|
+
Locates the directory whose path tail matches `source` (split on `.`),
|
|
65
|
+
rewrites every `import …` / `from … import …` that starts with `source`
|
|
66
|
+
to use `target` instead, and finally renames the directory on disk.
|
|
67
|
+
|
|
68
|
+
Scope = a single workdir. Cross-package propagation (suite-wide) is
|
|
69
|
+
delegated to a later step via the event bus.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
workdir_path: Path,
|
|
75
|
+
source: str,
|
|
76
|
+
target: str,
|
|
77
|
+
*,
|
|
78
|
+
dry_run: bool = True,
|
|
79
|
+
) -> None:
|
|
80
|
+
self.workdir_path = workdir_path
|
|
81
|
+
self.source = source
|
|
82
|
+
self.target = target
|
|
83
|
+
self.dry_run = dry_run
|
|
84
|
+
self._source_parts: tuple[str, ...] = tuple(source.split("."))
|
|
85
|
+
self._target_parts: tuple[str, ...] = tuple(target.split("."))
|
|
86
|
+
if not all(self._source_parts) or not all(self._target_parts):
|
|
87
|
+
raise ValueError("source and target must be dotted names without empty parts")
|
|
88
|
+
|
|
89
|
+
def find_package_dir(self) -> Path:
|
|
90
|
+
"""Return the unique directory matching `source` as a Python package."""
|
|
91
|
+
n = len(self._source_parts) # hoisted: avoid recomputing per candidate
|
|
92
|
+
matches: list[Path] = []
|
|
93
|
+
for init_file in self.workdir_path.rglob("__init__.py"):
|
|
94
|
+
if any(part in _SKIP_DIR_NAMES for part in init_file.parts):
|
|
95
|
+
continue
|
|
96
|
+
pkg_dir = init_file.parent
|
|
97
|
+
if len(pkg_dir.parts) < n:
|
|
98
|
+
continue
|
|
99
|
+
if pkg_dir.parts[-n:] == self._source_parts: # tuple cmp, no Path alloc
|
|
100
|
+
matches.append(pkg_dir)
|
|
101
|
+
|
|
102
|
+
if not matches:
|
|
103
|
+
raise FileNotFoundError(
|
|
104
|
+
f"No package directory matching '{self.source}' under {self.workdir_path}"
|
|
105
|
+
)
|
|
106
|
+
if len(matches) > 1:
|
|
107
|
+
joined = "\n ".join(str(m) for m in matches)
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Multiple packages match '{self.source}', refine the dotted name:\n {joined}"
|
|
110
|
+
)
|
|
111
|
+
return matches[0]
|
|
112
|
+
|
|
113
|
+
def run(self) -> RenameReport:
|
|
114
|
+
report = RenameReport()
|
|
115
|
+
package_dir = self.find_package_dir()
|
|
116
|
+
report.package_dir_old = package_dir
|
|
117
|
+
report.package_dir_new = package_dir.parent / self._target_parts[-1]
|
|
118
|
+
|
|
119
|
+
for py_file in self._iter_python_files():
|
|
120
|
+
report.files_scanned += 1
|
|
121
|
+
new_source = self._rewrite_imports(py_file)
|
|
122
|
+
if new_source is None:
|
|
123
|
+
continue
|
|
124
|
+
report.files_changed.append(py_file)
|
|
125
|
+
if not self.dry_run:
|
|
126
|
+
py_file.write_text(new_source, encoding="utf-8")
|
|
127
|
+
|
|
128
|
+
if not self.dry_run:
|
|
129
|
+
# Move happens AFTER rewrites: a file under the package itself
|
|
130
|
+
# would otherwise be looked up at the new path that doesn't exist
|
|
131
|
+
# yet during the rewrite pass.
|
|
132
|
+
package_dir.rename(report.package_dir_new)
|
|
133
|
+
|
|
134
|
+
return report
|
|
135
|
+
|
|
136
|
+
def _iter_python_files(self):
|
|
137
|
+
stack: list[Path] = [self.workdir_path]
|
|
138
|
+
while stack:
|
|
139
|
+
current = stack.pop()
|
|
140
|
+
try:
|
|
141
|
+
entries = list(current.iterdir())
|
|
142
|
+
except (PermissionError, FileNotFoundError):
|
|
143
|
+
continue
|
|
144
|
+
for entry in entries:
|
|
145
|
+
if entry.is_dir():
|
|
146
|
+
if entry.name not in _SKIP_DIR_NAMES:
|
|
147
|
+
stack.append(entry)
|
|
148
|
+
elif entry.suffix == ".py":
|
|
149
|
+
yield entry
|
|
150
|
+
|
|
151
|
+
def _rewrite_imports(self, py_file: Path) -> str | None:
|
|
152
|
+
"""Return rewritten source, or None when no import touched this file."""
|
|
153
|
+
source = py_file.read_text(encoding="utf-8")
|
|
154
|
+
try:
|
|
155
|
+
module = cst.parse_module(source)
|
|
156
|
+
except cst.ParserSyntaxError:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
transformer = _ImportRenameTransformer(
|
|
160
|
+
old_parts=self._source_parts,
|
|
161
|
+
new_parts=self._target_parts,
|
|
162
|
+
)
|
|
163
|
+
new_module = module.visit(transformer)
|
|
164
|
+
if not transformer.changed:
|
|
165
|
+
return None
|
|
166
|
+
return new_module.code
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class _ImportRenameTransformer(cst.CSTTransformer):
|
|
170
|
+
"""Rewrites import statements whose dotted name starts with `old_parts`."""
|
|
171
|
+
|
|
172
|
+
def __init__(self, old_parts: tuple[str, ...], new_parts: tuple[str, ...]) -> None:
|
|
173
|
+
super().__init__()
|
|
174
|
+
self.old_parts = old_parts
|
|
175
|
+
self.new_parts = new_parts
|
|
176
|
+
self._n = len(old_parts) # cached: avoid repeated len() + attr-lookup
|
|
177
|
+
self.changed = False
|
|
178
|
+
|
|
179
|
+
def leave_Import(
|
|
180
|
+
self,
|
|
181
|
+
original_node: cst.Import,
|
|
182
|
+
updated_node: cst.Import,
|
|
183
|
+
) -> cst.Import:
|
|
184
|
+
new_names = []
|
|
185
|
+
touched = False
|
|
186
|
+
for alias in updated_node.names:
|
|
187
|
+
parts = _dotted_to_parts(alias.name)
|
|
188
|
+
if parts is None or not self._starts_with(parts):
|
|
189
|
+
new_names.append(alias)
|
|
190
|
+
continue
|
|
191
|
+
new_parts = self.new_parts + parts[self._n:]
|
|
192
|
+
new_names.append(alias.with_changes(name=_parts_to_dotted(new_parts)))
|
|
193
|
+
touched = True
|
|
194
|
+
if not touched:
|
|
195
|
+
return updated_node
|
|
196
|
+
self.changed = True
|
|
197
|
+
return updated_node.with_changes(names=new_names)
|
|
198
|
+
|
|
199
|
+
def leave_ImportFrom(
|
|
200
|
+
self,
|
|
201
|
+
original_node: cst.ImportFrom,
|
|
202
|
+
updated_node: cst.ImportFrom,
|
|
203
|
+
) -> cst.ImportFrom:
|
|
204
|
+
# `from . import X` has module=None; relative imports are out of scope.
|
|
205
|
+
if updated_node.module is None:
|
|
206
|
+
return updated_node
|
|
207
|
+
parts = _dotted_to_parts(updated_node.module)
|
|
208
|
+
if parts is None or not self._starts_with(parts):
|
|
209
|
+
return updated_node
|
|
210
|
+
new_parts = self.new_parts + parts[self._n:]
|
|
211
|
+
self.changed = True
|
|
212
|
+
return updated_node.with_changes(module=_parts_to_dotted(new_parts))
|
|
213
|
+
|
|
214
|
+
def _starts_with(self, parts: tuple[str, ...]) -> bool:
|
|
215
|
+
return len(parts) >= self._n and parts[: self._n] == self.old_parts
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SymbolKind(str, Enum):
|
|
7
|
+
"""The kind of symbol targeted by a rename.
|
|
8
|
+
|
|
9
|
+
Step 1 only implements PACKAGE; the other members are listed so callers can
|
|
10
|
+
already pass `--kind class` and get a clear `NotImplementedError` instead
|
|
11
|
+
of a parsing error.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
PACKAGE = "package"
|
|
15
|
+
MODULE = "module"
|
|
16
|
+
CLASS = "class"
|
|
17
|
+
FUNCTION = "function"
|
|
18
|
+
METHOD = "method"
|
|
19
|
+
CONSTANT = "constant"
|
|
@@ -32,14 +32,27 @@ class AbstractPythonCodeSelection(AbstractCodeFromDirectorySelection):
|
|
|
32
32
|
MIN_SIZE_BYTES: ClassVar[int] = 50
|
|
33
33
|
|
|
34
34
|
def get_excluded_file_names(self) -> tuple[str, ...]:
|
|
35
|
-
|
|
35
|
+
try:
|
|
36
|
+
return self._excl_file_names # type: ignore[attr-defined]
|
|
37
|
+
except AttributeError:
|
|
38
|
+
self._excl_file_names: tuple[str, ...] = (
|
|
39
|
+
super().get_excluded_file_names() + ("__init__.py",)
|
|
40
|
+
)
|
|
41
|
+
return self._excl_file_names
|
|
36
42
|
|
|
37
43
|
def get_excluded_path_fragments(self) -> tuple[str, ...]:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
try:
|
|
45
|
+
return self._excl_path_frags # type: ignore[attr-defined]
|
|
46
|
+
except AttributeError:
|
|
47
|
+
self._excl_path_frags: tuple[str, ...] = (
|
|
48
|
+
super().get_excluded_path_fragments()
|
|
49
|
+
+ (
|
|
50
|
+
"__pycache__",
|
|
51
|
+
"/tests/",
|
|
52
|
+
"/build/",
|
|
53
|
+
"/dist/",
|
|
54
|
+
"/.venv/",
|
|
55
|
+
"/.pdm-build/",
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
return self._excl_path_frags
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import ast
|
|
3
4
|
from typing import TYPE_CHECKING, ClassVar
|
|
4
5
|
|
|
5
6
|
from wexample_helpers.decorator.base_class import base_class
|
|
@@ -67,8 +68,6 @@ class PythonCodePerformanceSelection(AbstractPythonCodeSelection):
|
|
|
67
68
|
files isn't what this sweep is for, the operator fixes those first.
|
|
68
69
|
Other I/O errors (FileNotFoundError, PermissionError) propagate.
|
|
69
70
|
"""
|
|
70
|
-
import ast
|
|
71
|
-
|
|
72
71
|
if not super().is_candidate(item):
|
|
73
72
|
return False
|
|
74
73
|
try:
|
|
@@ -104,8 +103,6 @@ class PythonCodePerformanceSelection(AbstractPythonCodeSelection):
|
|
|
104
103
|
already visits every descendant function independently. Keeps each
|
|
105
104
|
check decoupled from its container.
|
|
106
105
|
"""
|
|
107
|
-
import ast
|
|
108
|
-
|
|
109
106
|
for deco in node.decorator_list:
|
|
110
107
|
deco_name = (
|
|
111
108
|
deco.id
|
|
@@ -117,21 +114,23 @@ class PythonCodePerformanceSelection(AbstractPythonCodeSelection):
|
|
|
117
114
|
if deco_name == "abstractmethod":
|
|
118
115
|
return True
|
|
119
116
|
|
|
120
|
-
body =
|
|
117
|
+
body = node.body
|
|
118
|
+
start = 0
|
|
121
119
|
if (
|
|
122
120
|
body
|
|
123
121
|
and isinstance(body[0], ast.Expr)
|
|
124
122
|
and isinstance(body[0].value, ast.Constant)
|
|
125
123
|
and isinstance(body[0].value.value, str)
|
|
126
124
|
):
|
|
127
|
-
|
|
125
|
+
start = 1
|
|
128
126
|
|
|
129
|
-
|
|
127
|
+
effective_len = len(body) - start
|
|
128
|
+
if effective_len == 0:
|
|
130
129
|
return True
|
|
131
|
-
if
|
|
130
|
+
if effective_len != 1:
|
|
132
131
|
return False
|
|
133
132
|
|
|
134
|
-
stmt = body[
|
|
133
|
+
stmt = body[start]
|
|
135
134
|
|
|
136
135
|
if isinstance(stmt, ast.Pass):
|
|
137
136
|
return True
|
|
@@ -29,27 +29,24 @@ def python__service__install(
|
|
|
29
29
|
context: ExecutionContext,
|
|
30
30
|
service: AppService,
|
|
31
31
|
) -> None:
|
|
32
|
-
project_name = service.app_workdir.get_project_name()
|
|
33
|
-
|
|
34
32
|
config_file = service.app_workdir.get_config_file()
|
|
35
33
|
config = config_file.read_config()
|
|
36
34
|
|
|
37
|
-
from wexample_app.const.globals import WORKDIR_SETUP_DIR
|
|
38
|
-
|
|
39
35
|
if config.search("docker.images").is_none():
|
|
36
|
+
from wexample_app.const.globals import WORKDIR_SETUP_DIR
|
|
37
|
+
|
|
38
|
+
project_name = service.app_workdir.get_project_name()
|
|
39
|
+
docker_images_dir = WORKDIR_SETUP_DIR / "docker" / "images"
|
|
40
|
+
|
|
40
41
|
config.set_by_path(
|
|
41
42
|
"docker.images",
|
|
42
43
|
{
|
|
43
44
|
"base": {
|
|
44
|
-
"dockerfile": str(
|
|
45
|
-
WORKDIR_SETUP_DIR / "docker" / "images" / "Dockerfile.base"
|
|
46
|
-
),
|
|
45
|
+
"dockerfile": str(docker_images_dir / "Dockerfile.base"),
|
|
47
46
|
"tag": f"{project_name}:local",
|
|
48
47
|
},
|
|
49
48
|
"develop": {
|
|
50
|
-
"dockerfile": str(
|
|
51
|
-
WORKDIR_SETUP_DIR / "docker" / "images" / "Dockerfile.develop"
|
|
52
|
-
),
|
|
49
|
+
"dockerfile": str(docker_images_dir / "Dockerfile.develop"),
|
|
53
50
|
"tag": f"{project_name}-dev:local",
|
|
54
51
|
"depends_on": "base",
|
|
55
52
|
},
|
|
@@ -53,16 +53,18 @@ def python__service__setup(
|
|
|
53
53
|
return
|
|
54
54
|
|
|
55
55
|
ordered = resolve_build_order(builds)
|
|
56
|
+
app_path_str = str(app_path)
|
|
56
57
|
|
|
57
58
|
for build_name in ordered:
|
|
58
59
|
build = builds[build_name]
|
|
59
60
|
dockerfile = str(app_path / build["dockerfile"])
|
|
60
61
|
tag = build["tag"]
|
|
61
62
|
cmd = ["docker", "build", "-f", dockerfile, "-t", tag]
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
depends_on = build.get("depends_on")
|
|
64
|
+
if depends_on is not None:
|
|
65
|
+
parent_tag = builds[depends_on]["tag"]
|
|
64
66
|
cmd += ["--build-arg", f"BASE_IMAGE={parent_tag}"]
|
|
65
|
-
cmd.append(
|
|
67
|
+
cmd.append(app_path_str)
|
|
66
68
|
context.io.log(f"Building image '{build_name}' → {tag}")
|
|
67
69
|
subprocess.run(cmd, check=True)
|
|
68
70
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
3
5
|
from wexample_helpers.decorator.base_class import base_class
|
|
4
6
|
from wexample_wex_addon_app.workdir.mixin.abstract_profiling_workdir_mixin import (
|
|
5
7
|
AbstractProfilingWorkdirMixin,
|
|
@@ -24,7 +26,8 @@ class WithProfilingPythonWorkdirMixin(AbstractProfilingWorkdirMixin):
|
|
|
24
26
|
import json
|
|
25
27
|
import subprocess
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
workdir_path = self.get_path()
|
|
30
|
+
bench_output_path = workdir_path / self._BENCH_OUTPUT_FILENAME
|
|
28
31
|
python = self._get_profiling_python()
|
|
29
32
|
|
|
30
33
|
bench_result = subprocess.run(
|
|
@@ -39,7 +42,7 @@ class WithProfilingPythonWorkdirMixin(AbstractProfilingWorkdirMixin):
|
|
|
39
42
|
],
|
|
40
43
|
capture_output=True,
|
|
41
44
|
text=True,
|
|
42
|
-
cwd=str(
|
|
45
|
+
cwd=str(workdir_path),
|
|
43
46
|
)
|
|
44
47
|
|
|
45
48
|
if not bench_output_path.exists():
|
|
@@ -69,27 +72,23 @@ class WithProfilingPythonWorkdirMixin(AbstractProfilingWorkdirMixin):
|
|
|
69
72
|
|
|
70
73
|
def _get_profiling_python(self) -> Path:
|
|
71
74
|
import sys
|
|
72
|
-
from pathlib import Path
|
|
73
75
|
|
|
74
76
|
return Path(sys.executable)
|
|
75
77
|
|
|
76
78
|
def _parse_profiling_output(self, raw: dict) -> dict:
|
|
77
|
-
|
|
78
|
-
for bench in raw.get("benchmarks", []):
|
|
79
|
+
def _bench_entry(bench: dict) -> dict:
|
|
79
80
|
stats = bench.get("stats", {})
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
)
|
|
81
|
+
return {
|
|
82
|
+
"name": bench.get("name"),
|
|
83
|
+
"min_ms": round(stats.get("min", 0) * 1000, 3),
|
|
84
|
+
"mean_ms": round(stats.get("mean", 0) * 1000, 3),
|
|
85
|
+
"median_ms": round(stats.get("median", 0) * 1000, 3),
|
|
86
|
+
"max_ms": round(stats.get("max", 0) * 1000, 3),
|
|
87
|
+
"rounds": stats.get("rounds", 0),
|
|
88
|
+
}
|
|
90
89
|
|
|
91
90
|
return {
|
|
92
91
|
"language": "python",
|
|
93
92
|
"tool": "pytest-benchmark",
|
|
94
|
-
"entries":
|
|
93
|
+
"entries": [_bench_entry(b) for b in raw.get("benchmarks", [])],
|
|
95
94
|
}
|
|
@@ -156,10 +156,11 @@ class PythonPackageWorkdir(PythonWorkdir):
|
|
|
156
156
|
import re
|
|
157
157
|
|
|
158
158
|
pkg = searched_package.get_package_import_name()
|
|
159
|
+
escaped = re.escape(pkg)
|
|
159
160
|
pattern = (
|
|
160
161
|
rf"(?m)^\s*(?:"
|
|
161
|
-
rf"from\s+{
|
|
162
|
-
rf"|import\s+{
|
|
162
|
+
rf"from\s+{escaped}(?:\.[\w\.]+)?\s+import\s+"
|
|
163
|
+
rf"|import\s+{escaped}(?:\.[\w\.]+)?(?:\s+as\s+\w+)?\b"
|
|
163
164
|
rf")"
|
|
164
165
|
)
|
|
165
166
|
return self.search_in_codebase(pattern, regex=True, flags=re.MULTILINE)
|
|
@@ -198,7 +199,8 @@ class PythonPackageWorkdir(PythonWorkdir):
|
|
|
198
199
|
import griffe
|
|
199
200
|
|
|
200
201
|
module_name = self.get_package_name().replace("-", "_")
|
|
201
|
-
|
|
202
|
+
path = self.get_path()
|
|
203
|
+
repo_path = str(path)
|
|
202
204
|
|
|
203
205
|
previous = griffe.load_git(
|
|
204
206
|
module_name,
|
|
@@ -208,10 +210,10 @@ class PythonPackageWorkdir(PythonWorkdir):
|
|
|
208
210
|
)
|
|
209
211
|
current = griffe.load(
|
|
210
212
|
module_name,
|
|
211
|
-
search_paths=[str(
|
|
213
|
+
search_paths=[str(path / "src")],
|
|
212
214
|
)
|
|
213
215
|
|
|
214
|
-
if
|
|
216
|
+
if any(griffe.find_breaking_changes(previous, current)):
|
|
215
217
|
return UPGRADE_TYPE_MAJOR
|
|
216
218
|
|
|
217
219
|
return UPGRADE_TYPE_INTERMEDIATE
|
|
@@ -353,7 +355,7 @@ class PythonPackageWorkdir(PythonWorkdir):
|
|
|
353
355
|
)
|
|
354
356
|
python_install_dependencies_in_venv(
|
|
355
357
|
venv_path=venv_path,
|
|
356
|
-
names=
|
|
358
|
+
names=toml_file.optional_group_array(group="dev"),
|
|
357
359
|
)
|
|
358
360
|
|
|
359
361
|
self.subtitle(
|
|
@@ -489,7 +491,7 @@ class PythonPackageWorkdir(PythonWorkdir):
|
|
|
489
491
|
)
|
|
490
492
|
|
|
491
493
|
PollingCallbackManager(
|
|
492
|
-
callback=lambda:
|
|
494
|
+
callback=lambda: gateway.has_version(package, version) or None,
|
|
493
495
|
max_attempts=max_attempts,
|
|
494
496
|
delay_seconds_callback=lambda _attempt: delay_seconds,
|
|
495
497
|
on_retry_callback=on_retry,
|
|
@@ -52,9 +52,8 @@ class PythonPackagesSuiteWorkdir(FrameworkPackageSuiteWorkdir):
|
|
|
52
52
|
for n in sorted(nodes):
|
|
53
53
|
G.add_node(n)
|
|
54
54
|
for src in sorted(dependencies_map.keys()):
|
|
55
|
-
for dst in sorted(dependencies_map
|
|
56
|
-
|
|
57
|
-
G.add_edge(src, dst)
|
|
55
|
+
for dst in sorted(dependencies_map[src]):
|
|
56
|
+
G.add_edge(src, dst)
|
|
58
57
|
|
|
59
58
|
if not (G.has_node(start) and G.has_node(target)):
|
|
60
59
|
return []
|