wexample-wex-addon-dev-python 0.0.60__py3-none-any.whl → 0.0.62__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/config_value/python_package_readme_config_value.py +27 -226
- wexample_wex_addon_dev_python/const/python.py +0 -0
- wexample_wex_addon_dev_python/file/python_app_iml_file.py +41 -0
- wexample_wex_addon_dev_python/file/{python_package_toml_file.py → python_pyproject_toml_file.py} +35 -35
- wexample_wex_addon_dev_python/resources/__init__.py +0 -0
- wexample_wex_addon_dev_python/resources/readme_templates/__init__.py +0 -0
- wexample_wex_addon_dev_python/resources/readme_templates/tests.md.j2 +0 -0
- wexample_wex_addon_dev_python/workdir/python_package_workdir.py +90 -220
- wexample_wex_addon_dev_python/workdir/python_packages_suite_workdir.py +0 -88
- wexample_wex_addon_dev_python/workdir/python_workdir.py +63 -67
- {wexample_wex_addon_dev_python-0.0.60.dist-info → wexample_wex_addon_dev_python-0.0.62.dist-info}/METADATA +72 -14
- {wexample_wex_addon_dev_python-0.0.60.dist-info → wexample_wex_addon_dev_python-0.0.62.dist-info}/RECORD +10 -10
- wexample_wex_addon_dev_python/resources/readme_templates/title.md.j2 +0 -5
- {wexample_wex_addon_dev_python-0.0.60.dist-info → wexample_wex_addon_dev_python-0.0.62.dist-info}/WHEEL +0 -0
- {wexample_wex_addon_dev_python-0.0.60.dist-info → wexample_wex_addon_dev_python-0.0.62.dist-info}/entry_points.txt +0 -0
|
@@ -2,248 +2,49 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from wexample_filestate.config_value.readme_content_config_value import (
|
|
6
|
-
ReadmeContentConfigValue,
|
|
7
|
-
)
|
|
8
|
-
from wexample_helpers.classes.field import public_field
|
|
9
5
|
from wexample_helpers.decorator.base_class import base_class
|
|
6
|
+
from wexample_wex_addon_app.config_value.app_readme_config_value import (
|
|
7
|
+
AppReadmeConfigValue,
|
|
8
|
+
)
|
|
10
9
|
|
|
11
10
|
if TYPE_CHECKING:
|
|
12
|
-
|
|
13
|
-
PythonPackageWorkdir,
|
|
14
|
-
)
|
|
11
|
+
pass
|
|
15
12
|
|
|
16
13
|
|
|
17
14
|
@base_class
|
|
18
|
-
class PythonPackageReadmeContentConfigValue(
|
|
19
|
-
|
|
20
|
-
description="The python package workdir"
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
def get_templates(self) -> list[str] | None:
|
|
24
|
-
# Prepare context for Jinja2 rendering
|
|
25
|
-
context = self._get_template_context()
|
|
26
|
-
|
|
27
|
-
# Define fixed order of README sections
|
|
28
|
-
section_names = [
|
|
29
|
-
"title",
|
|
30
|
-
"table-of-contents",
|
|
31
|
-
"status-compatibility",
|
|
32
|
-
"prerequisites",
|
|
33
|
-
"installation",
|
|
34
|
-
"quickstart",
|
|
35
|
-
"basic-usage",
|
|
36
|
-
"configuration",
|
|
37
|
-
"logging",
|
|
38
|
-
"api-reference",
|
|
39
|
-
"examples",
|
|
40
|
-
"tests",
|
|
41
|
-
"code-quality",
|
|
42
|
-
"versioning",
|
|
43
|
-
"changelog",
|
|
44
|
-
"migration-notes",
|
|
45
|
-
"roadmap",
|
|
46
|
-
"troubleshooting",
|
|
47
|
-
"security",
|
|
48
|
-
"privacy",
|
|
49
|
-
"support",
|
|
50
|
-
"contribution-guidelines",
|
|
51
|
-
"maintainers",
|
|
52
|
-
"license",
|
|
53
|
-
"useful-links",
|
|
54
|
-
"suite-integration",
|
|
55
|
-
"compatibility-matrix",
|
|
56
|
-
"requirements",
|
|
57
|
-
"dependencies",
|
|
58
|
-
"links",
|
|
59
|
-
"suite-signature",
|
|
60
|
-
]
|
|
61
|
-
|
|
62
|
-
# First pass: collect available sections (excluding title and table-of-contents)
|
|
63
|
-
available_sections = []
|
|
64
|
-
for section_name in section_names:
|
|
65
|
-
if section_name not in ["title", "table-of-contents"]:
|
|
66
|
-
# Check if section exists
|
|
67
|
-
if self._section_exists(section_name):
|
|
68
|
-
available_sections.append(
|
|
69
|
-
{
|
|
70
|
-
"name": section_name,
|
|
71
|
-
"title": self._section_name_to_title(section_name),
|
|
72
|
-
"anchor": section_name.replace("_", "-"),
|
|
73
|
-
}
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
# Add available sections to context for table-of-contents
|
|
77
|
-
context["available_sections"] = available_sections
|
|
15
|
+
class PythonPackageReadmeContentConfigValue(AppReadmeConfigValue):
|
|
16
|
+
"""README generation for Python packages."""
|
|
78
17
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
section_content = self._render_readme_section(section_name, context)
|
|
83
|
-
if section_content:
|
|
84
|
-
rendered_content += f"{section_content}\n\n"
|
|
18
|
+
def _get_app_description(self) -> str:
|
|
19
|
+
"""Extract description from pyproject.toml."""
|
|
20
|
+
return self.workdir.get_app_config().get("project", {}).get("description")
|
|
85
21
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# Use TOMLDocument from the workdir
|
|
90
|
-
doc = self.workdir.get_project_config()
|
|
91
|
-
project = doc.get("project", {}) if isinstance(doc, dict) else {}
|
|
92
|
-
|
|
93
|
-
# Extract information
|
|
94
|
-
description = project.get("description", "")
|
|
95
|
-
python_version = project.get("requires-python", "")
|
|
96
|
-
dependencies = project.get("dependencies", [])
|
|
22
|
+
def _get_app_homepage(self) -> str:
|
|
23
|
+
"""Extract homepage URL from pyproject.toml."""
|
|
24
|
+
project = self.workdir.get_app_config()
|
|
97
25
|
urls = (
|
|
98
26
|
project.get("urls", {}) if isinstance(project.get("urls", {}), dict) else {}
|
|
99
27
|
)
|
|
100
|
-
|
|
101
|
-
|
|
28
|
+
return urls.get("homepage") or urls.get("Homepage") or ""
|
|
29
|
+
|
|
30
|
+
def _get_project_license(self) -> str | None:
|
|
31
|
+
"""Extract license information from pyproject.toml."""
|
|
32
|
+
project = self.workdir.get_app_config()
|
|
102
33
|
license_field = project.get("license", {})
|
|
103
34
|
if isinstance(license_field, dict):
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
107
|
-
else:
|
|
108
|
-
license_info = str(license_field) if license_field else ""
|
|
109
|
-
|
|
110
|
-
# Format dependencies list
|
|
111
|
-
deps_list = "\n".join([f"- {dep}" for dep in dependencies])
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
"package_name": self.workdir.get_package_name(),
|
|
115
|
-
"version": self.workdir.get_project_version(),
|
|
116
|
-
"description": description,
|
|
117
|
-
"python_version": python_version,
|
|
118
|
-
"dependencies": dependencies,
|
|
119
|
-
"deps_list": deps_list,
|
|
120
|
-
"homepage": homepage,
|
|
121
|
-
"license_info": license_info,
|
|
122
|
-
"workdir": self.workdir,
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
def _render_readme_section(self, section_name: str, context: dict) -> str | None:
|
|
126
|
-
"""
|
|
127
|
-
Render a README section from .md or .md.j2 file with Jinja2 support.
|
|
128
|
-
|
|
129
|
-
Searches in three levels (in order):
|
|
130
|
-
1. Package-level templates
|
|
131
|
-
2. Suite-level templates
|
|
132
|
-
3. Default templates (bundled with the module)
|
|
133
|
-
|
|
134
|
-
Tries .md.j2 first, then .md. Both formats support Jinja2 variables.
|
|
135
|
-
|
|
136
|
-
Args:
|
|
137
|
-
section_name: Name of the section (without extension)
|
|
138
|
-
context: Jinja2 context variables for rendering
|
|
139
|
-
|
|
140
|
-
Returns:
|
|
141
|
-
Rendered content or None if section file not found
|
|
142
|
-
"""
|
|
143
|
-
from pathlib import Path
|
|
144
|
-
|
|
145
|
-
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
|
|
146
|
-
from wexample_app.const.globals import WORKDIR_SETUP_DIR
|
|
147
|
-
|
|
148
|
-
workdir_path = self.workdir.get_path()
|
|
149
|
-
|
|
150
|
-
search_paths = [
|
|
151
|
-
workdir_path / WORKDIR_SETUP_DIR / "knowledge" / "readme", # Package-level
|
|
152
|
-
]
|
|
35
|
+
return license_field.get("text", "") or license_field.get("file", "")
|
|
36
|
+
return str(license_field) if license_field else ""
|
|
153
37
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if suite_path is not None:
|
|
157
|
-
search_paths.append(
|
|
158
|
-
suite_path
|
|
159
|
-
/ WORKDIR_SETUP_DIR
|
|
160
|
-
/ "knowledge"
|
|
161
|
-
/ "package-readme", # Suite-level
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
# Add default templates path (bundled with the module)
|
|
165
|
-
default_templates_path = (
|
|
166
|
-
Path(__file__).parent.parent / "resources" / "readme_templates"
|
|
167
|
-
)
|
|
168
|
-
search_paths.append(default_templates_path)
|
|
169
|
-
|
|
170
|
-
# Try .md.j2 first (Jinja2 template)
|
|
171
|
-
for search_path in search_paths:
|
|
172
|
-
if not search_path.exists():
|
|
173
|
-
continue
|
|
174
|
-
|
|
175
|
-
env = Environment(loader=FileSystemLoader(str(search_path)))
|
|
176
|
-
try:
|
|
177
|
-
template = env.get_template(f"{section_name}.md.j2")
|
|
178
|
-
return template.render(context)
|
|
179
|
-
except TemplateNotFound:
|
|
180
|
-
pass
|
|
181
|
-
|
|
182
|
-
# Try .md (static markdown, still rendered with Jinja2)
|
|
183
|
-
for search_path in search_paths:
|
|
184
|
-
md_path = search_path / f"{section_name}.md"
|
|
185
|
-
if md_path.exists():
|
|
186
|
-
content = md_path.read_text(encoding="utf-8")
|
|
187
|
-
env = Environment(loader=FileSystemLoader(str(search_path)))
|
|
188
|
-
template = env.from_string(content)
|
|
189
|
-
return template.render(context)
|
|
190
|
-
|
|
191
|
-
return None
|
|
192
|
-
|
|
193
|
-
def _section_exists(self, section_name: str) -> bool:
|
|
194
|
-
"""
|
|
195
|
-
Check if a section file exists (.md or .md.j2).
|
|
196
|
-
|
|
197
|
-
Searches in three levels:
|
|
198
|
-
1. Package-level templates
|
|
199
|
-
2. Suite-level templates
|
|
200
|
-
3. Default templates (bundled with the module)
|
|
201
|
-
|
|
202
|
-
Args:
|
|
203
|
-
section_name: Name of the section (without extension)
|
|
38
|
+
def _get_template_context(self) -> dict:
|
|
39
|
+
"""Build template context with Python-specific variables.
|
|
204
40
|
|
|
205
|
-
|
|
206
|
-
True if section file exists, False otherwise
|
|
41
|
+
Adds python_version to the base context.
|
|
207
42
|
"""
|
|
208
|
-
|
|
43
|
+
context = super()._get_template_context()
|
|
209
44
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
search_paths = [
|
|
215
|
-
workdir_path / WORKDIR_SETUP_DIR / "knowledge" / "readme",
|
|
216
|
-
]
|
|
217
|
-
|
|
218
|
-
# Package may have a suite.
|
|
219
|
-
suite_path = self.workdir.find_suite_workdir_path()
|
|
220
|
-
if suite_path is not None:
|
|
221
|
-
search_paths.append(
|
|
222
|
-
suite_path / WORKDIR_SETUP_DIR / "knowledge" / "package-readme",
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
# Add default templates path (bundled with the module)
|
|
226
|
-
default_templates_path = (
|
|
227
|
-
Path(__file__).parent.parent / "resources" / "readme_templates"
|
|
45
|
+
# Add Python-specific variable
|
|
46
|
+
context["python_version"] = (
|
|
47
|
+
self.workdir.get_app_config().get("project", {}).get("requires-python", "")
|
|
228
48
|
)
|
|
229
|
-
search_paths.append(default_templates_path)
|
|
230
|
-
|
|
231
|
-
for search_path in search_paths:
|
|
232
|
-
if (search_path / f"{section_name}.md.j2").exists():
|
|
233
|
-
return True
|
|
234
|
-
if (search_path / f"{section_name}.md").exists():
|
|
235
|
-
return True
|
|
236
49
|
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
def _section_name_to_title(self, section_name: str) -> str:
|
|
240
|
-
"""
|
|
241
|
-
Convert section name to human-readable title.
|
|
242
|
-
|
|
243
|
-
Args:
|
|
244
|
-
section_name: Section name (e.g., "basic-usage")
|
|
245
|
-
|
|
246
|
-
Returns:
|
|
247
|
-
Human-readable title (e.g., "Basic Usage")
|
|
248
|
-
"""
|
|
249
|
-
return section_name.replace("-", " ").replace("_", " ").title()
|
|
50
|
+
return context
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import Any, ClassVar
|
|
5
|
+
|
|
6
|
+
from wexample_app.item.file.iml_file import ImlFile
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PythonAppImlFile(ImlFile):
|
|
10
|
+
"""
|
|
11
|
+
IntelliJ IDEA .iml helper tailored for Python apps (src/tests layout, python module type).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
MODULE_TYPE: ClassVar[str] = "PYTHON_MODULE"
|
|
15
|
+
|
|
16
|
+
def _default_exclude_folders(self) -> Iterable[dict[str, Any]]:
|
|
17
|
+
return (
|
|
18
|
+
{
|
|
19
|
+
"@url": f"{self.MODULE_DIR_URL}/dist",
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def _default_module_attributes(self) -> dict[str, str]:
|
|
24
|
+
attrs = super()._default_module_attributes()
|
|
25
|
+
attrs.setdefault("@type", self.MODULE_TYPE)
|
|
26
|
+
return attrs
|
|
27
|
+
|
|
28
|
+
def _default_order_entries(self) -> Iterable[dict[str, Any]]:
|
|
29
|
+
return ({"@type": "sourceFolder", "@forTests": "false"},)
|
|
30
|
+
|
|
31
|
+
def _default_source_folders(self) -> Iterable[dict[str, Any]]:
|
|
32
|
+
return (
|
|
33
|
+
{
|
|
34
|
+
"@url": f"{self.MODULE_DIR_URL}/src",
|
|
35
|
+
"@isTestSource": "false",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"@url": f"{self.MODULE_DIR_URL}/tests",
|
|
39
|
+
"@isTestSource": "true",
|
|
40
|
+
},
|
|
41
|
+
)
|
wexample_wex_addon_dev_python/file/{python_package_toml_file.py → python_pyproject_toml_file.py}
RENAMED
|
@@ -14,29 +14,44 @@ if TYPE_CHECKING:
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@base_class
|
|
17
|
-
class
|
|
17
|
+
class PythonPyprojectTomlFile(TomlFile):
|
|
18
18
|
def add_dependency(
|
|
19
|
-
self,
|
|
19
|
+
self,
|
|
20
|
+
package_name: str,
|
|
21
|
+
version: str,
|
|
22
|
+
operator: str = "==",
|
|
23
|
+
optional: bool = False,
|
|
24
|
+
group: None | str = None,
|
|
20
25
|
) -> bool:
|
|
21
26
|
from packaging.requirements import Requirement
|
|
22
27
|
from packaging.utils import canonicalize_name
|
|
23
28
|
from wexample_filestate_python.helpers.toml import toml_sort_string_array
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
spec = f"{package_name}{operator}{version}"
|
|
26
31
|
new_req = Requirement(spec)
|
|
27
32
|
new_name = canonicalize_name(new_req.name)
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
deps = self._get_deps_array(optional=optional, group=group)
|
|
35
|
+
|
|
36
|
+
# Look for existing dependency
|
|
37
|
+
old_spec = next(
|
|
38
|
+
(d for d in deps if canonicalize_name(Requirement(d).name) == new_name),
|
|
39
|
+
None,
|
|
40
|
+
)
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
# Remove old dep if exists
|
|
43
|
+
if old_spec:
|
|
44
|
+
deps.remove(old_spec)
|
|
36
45
|
|
|
46
|
+
# Add new version
|
|
37
47
|
deps.append(spec)
|
|
48
|
+
|
|
49
|
+
# Sort array
|
|
38
50
|
toml_sort_string_array(deps)
|
|
39
51
|
|
|
52
|
+
# Save file
|
|
53
|
+
self.write_parsed()
|
|
54
|
+
|
|
40
55
|
return old_spec != spec
|
|
41
56
|
|
|
42
57
|
def dumps(self, content: TOMLDocument | dict | None = None) -> str:
|
|
@@ -70,35 +85,21 @@ class PythonPackageTomlFile(TomlFile):
|
|
|
70
85
|
|
|
71
86
|
return self.find_closest(CodeBaseWorkdir)
|
|
72
87
|
|
|
73
|
-
def
|
|
88
|
+
def get_dependencies_versions(
|
|
74
89
|
self, optional: bool = False, group: str = "dev"
|
|
75
|
-
) ->
|
|
76
|
-
deps = self._get_deps_array(optional=optional, group=group)
|
|
77
|
-
return [str(x) for x in list(deps)]
|
|
78
|
-
|
|
79
|
-
def list_dependency_names(
|
|
80
|
-
self,
|
|
81
|
-
canonicalize_names: bool = True,
|
|
82
|
-
optional: bool = False,
|
|
83
|
-
group: str = "dev",
|
|
84
|
-
) -> list[str]:
|
|
85
|
-
"""Return dependency package names derived from list_dependencies().
|
|
86
|
-
|
|
87
|
-
If canonicalize_names is True, names are normalized using packaging's
|
|
88
|
-
canonicalize_name for robust comparisons (dash/underscore, case, etc.).
|
|
89
|
-
"""
|
|
90
|
+
) -> dict[str, str]:
|
|
90
91
|
from packaging.requirements import Requirement
|
|
91
92
|
from packaging.utils import canonicalize_name
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
94
|
+
deps = self._get_deps_array(optional=optional, group=group)
|
|
95
|
+
|
|
96
|
+
map = {}
|
|
97
|
+
for spec in list(deps):
|
|
98
|
+
req = Requirement(spec)
|
|
99
|
+
# name: version
|
|
100
|
+
map[canonicalize_name(req.name)] = str(req.specifier)
|
|
101
|
+
|
|
102
|
+
return map
|
|
102
103
|
|
|
103
104
|
def optional_group_array(self, group: str):
|
|
104
105
|
"""Ensure and return project.optional-dependencies[group] as multi-line array."""
|
|
@@ -305,7 +306,6 @@ class PythonPackageTomlFile(TomlFile):
|
|
|
305
306
|
|
|
306
307
|
toml_sort_string_array(dev_arr)
|
|
307
308
|
|
|
308
|
-
# --- Unified dependency accessors (runtime vs optional) ---
|
|
309
309
|
def _get_deps_array(self, optional: bool = False, group: str = "dev"):
|
|
310
310
|
"""Return TOML array for runtime deps or optional group (multiline)."""
|
|
311
311
|
return (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from wexample_filestate.const.disk import DiskItemType
|
|
7
|
+
from wexample_wex_addon_app.helpers.python import (
|
|
8
|
+
python_install_dependency_in_venv,
|
|
9
|
+
python_is_package_installed_editable_in_venv,
|
|
10
|
+
)
|
|
6
11
|
|
|
7
12
|
from wexample_wex_addon_dev_python.workdir.python_workdir import PythonWorkdir
|
|
8
13
|
|
|
@@ -20,165 +25,6 @@ if TYPE_CHECKING:
|
|
|
20
25
|
class PythonPackageWorkdir(PythonWorkdir):
|
|
21
26
|
_project_info_cache = None
|
|
22
27
|
|
|
23
|
-
def app_install(self, env: str | None = None, force: bool = False) -> bool:
|
|
24
|
-
from wexample_app.const.env import ENV_NAME_LOCAL
|
|
25
|
-
from wexample_helpers.helpers.shell import shell_run
|
|
26
|
-
|
|
27
|
-
# In local env, installs packages using pip.
|
|
28
|
-
if env == ENV_NAME_LOCAL:
|
|
29
|
-
toml_file = self.get_project_config_file()
|
|
30
|
-
# Get all dependencies from pyproject.toml
|
|
31
|
-
pyproject_toml_dependencies = toml_file.list_dependency_names()
|
|
32
|
-
|
|
33
|
-
suite_workdir = self.get_shallow_suite_workdir()
|
|
34
|
-
|
|
35
|
-
# Ensure venv is created and configured
|
|
36
|
-
app_path = self.get_path()
|
|
37
|
-
venv_path = app_path / ".venv"
|
|
38
|
-
|
|
39
|
-
# Check if venv exists and is valid (has bin/python)
|
|
40
|
-
venv_python = venv_path / "bin" / "python"
|
|
41
|
-
venv_is_valid = venv_path.exists() and venv_python.exists()
|
|
42
|
-
|
|
43
|
-
if not venv_is_valid:
|
|
44
|
-
# Remove corrupted/empty venv if it exists
|
|
45
|
-
if venv_path.exists():
|
|
46
|
-
self.log(f"Removing invalid venv at {venv_path}", indentation=1)
|
|
47
|
-
import shutil
|
|
48
|
-
|
|
49
|
-
shutil.rmtree(venv_path)
|
|
50
|
-
|
|
51
|
-
# Create new venv
|
|
52
|
-
shell_run(
|
|
53
|
-
cmd=["pdm", "venv", "create"],
|
|
54
|
-
cwd=app_path,
|
|
55
|
-
inherit_stdio=True,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
# Force PDM to use the local .venv
|
|
59
|
-
shell_run(
|
|
60
|
-
cmd=["pdm", "use", ".venv"],
|
|
61
|
-
cwd=app_path,
|
|
62
|
-
inherit_stdio=True,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
# Ensure pip is installed in the venv
|
|
66
|
-
shell_run(
|
|
67
|
-
cmd=[
|
|
68
|
-
".venv/bin/python",
|
|
69
|
-
"-m",
|
|
70
|
-
"ensurepip",
|
|
71
|
-
"--upgrade",
|
|
72
|
-
],
|
|
73
|
-
cwd=app_path,
|
|
74
|
-
inherit_stdio=True,
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
# The package is a part of a workdir, so we install manually individual package.
|
|
78
|
-
if suite_workdir:
|
|
79
|
-
# Get all packages from the suite ordered by dependencies (leaf -> trunk)
|
|
80
|
-
suite_packages = suite_workdir.get_ordered_packages()
|
|
81
|
-
suite_package_names = {pkg.get_package_name() for pkg in suite_packages}
|
|
82
|
-
|
|
83
|
-
# Collect all suite packages that need to be installed (including transitive dependencies)
|
|
84
|
-
suite_dependencies_ordered = self._collect_suite_dependencies(
|
|
85
|
-
pyproject_toml_dependencies, suite_workdir, suite_package_names
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
# External dependencies are those not in the suite
|
|
89
|
-
external_dependencies = [
|
|
90
|
-
dep
|
|
91
|
-
for dep in pyproject_toml_dependencies
|
|
92
|
-
if dep not in suite_package_names
|
|
93
|
-
]
|
|
94
|
-
|
|
95
|
-
# Install external packages first (normal install)
|
|
96
|
-
if external_dependencies:
|
|
97
|
-
self.subtitle(
|
|
98
|
-
f"Installing {len(external_dependencies)} external packages",
|
|
99
|
-
indentation=1,
|
|
100
|
-
)
|
|
101
|
-
for dep in external_dependencies:
|
|
102
|
-
self.log(f"Installing {dep}", indentation=2)
|
|
103
|
-
shell_run(
|
|
104
|
-
cmd=[
|
|
105
|
-
".venv/bin/python",
|
|
106
|
-
"-m",
|
|
107
|
-
"pip",
|
|
108
|
-
"install",
|
|
109
|
-
dep,
|
|
110
|
-
],
|
|
111
|
-
cwd=app_path,
|
|
112
|
-
inherit_stdio=True,
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
# Install suite packages in editable mode (leaf -> trunk order)
|
|
116
|
-
if suite_dependencies_ordered:
|
|
117
|
-
self.subtitle(
|
|
118
|
-
f"Installing {len(suite_dependencies_ordered)} suite packages in editable mode (leaf -> trunk)",
|
|
119
|
-
indentation=1,
|
|
120
|
-
)
|
|
121
|
-
for pkg in suite_dependencies_ordered:
|
|
122
|
-
package_path = pkg.get_path()
|
|
123
|
-
package_name = pkg.get_package_name()
|
|
124
|
-
|
|
125
|
-
# Check if package is already installed in editable mode at the correct path
|
|
126
|
-
if not force and self._is_package_installed_editable(
|
|
127
|
-
app_path, package_name, package_path
|
|
128
|
-
):
|
|
129
|
-
self.log(
|
|
130
|
-
f"Skipping {package_name} (already installed in editable mode)",
|
|
131
|
-
indentation=2,
|
|
132
|
-
)
|
|
133
|
-
continue
|
|
134
|
-
|
|
135
|
-
self.log(f"Installing {package_name}", indentation=2)
|
|
136
|
-
shell_run(
|
|
137
|
-
cmd=[
|
|
138
|
-
".venv/bin/python",
|
|
139
|
-
"-m",
|
|
140
|
-
"pip",
|
|
141
|
-
"install",
|
|
142
|
-
"-e",
|
|
143
|
-
str(package_path),
|
|
144
|
-
],
|
|
145
|
-
cwd=app_path,
|
|
146
|
-
inherit_stdio=True,
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
# Avoid error using -G
|
|
150
|
-
dev_group_name = "dev"
|
|
151
|
-
if (
|
|
152
|
-
len(
|
|
153
|
-
self.get_project_config_file().optional_group_array(
|
|
154
|
-
group=dev_group_name
|
|
155
|
-
)
|
|
156
|
-
)
|
|
157
|
-
> 0
|
|
158
|
-
):
|
|
159
|
-
self._pdm_update_lock_if_needed()
|
|
160
|
-
|
|
161
|
-
self.log(f"Installing dev group dependencies")
|
|
162
|
-
self._pdm_run_command(command=["install", "-G", dev_group_name])
|
|
163
|
-
else:
|
|
164
|
-
self.log(
|
|
165
|
-
"Skipping dev group install: group 'dev' not defined in pyproject.toml"
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
return True
|
|
169
|
-
|
|
170
|
-
# For non-local environments, use standard PDM install
|
|
171
|
-
return super().app_install(
|
|
172
|
-
env=env,
|
|
173
|
-
force=force,
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
def depends_from(self, package: PythonPackageWorkdir) -> bool:
|
|
177
|
-
for dependence_name in self.get_dependencies():
|
|
178
|
-
if package.get_package_name() == dependence_name:
|
|
179
|
-
return True
|
|
180
|
-
return False
|
|
181
|
-
|
|
182
28
|
def prepare_value(self, raw_value: DictConfig | None = None) -> DictConfig:
|
|
183
29
|
from wexample_helpers.helpers.array import array_dict_get_by
|
|
184
30
|
|
|
@@ -318,7 +164,7 @@ class PythonPackageWorkdir(PythonWorkdir):
|
|
|
318
164
|
pkg = suite_workdir.get_package(dep_name)
|
|
319
165
|
if pkg:
|
|
320
166
|
# Get dependencies of this suite package and recurse
|
|
321
|
-
pkg_dependencies = pkg.
|
|
167
|
+
pkg_dependencies = pkg.list_dependencies_names()
|
|
322
168
|
collect_recursive(pkg_dependencies)
|
|
323
169
|
|
|
324
170
|
# Start with direct dependencies from pyproject.toml
|
|
@@ -334,87 +180,111 @@ class PythonPackageWorkdir(PythonWorkdir):
|
|
|
334
180
|
|
|
335
181
|
return suite_deps_ordered
|
|
336
182
|
|
|
337
|
-
def
|
|
183
|
+
def _get_readme_content(self) -> ReadmeContentConfigValue | None:
|
|
184
|
+
from wexample_wex_addon_dev_python.config_value.python_package_readme_config_value import (
|
|
185
|
+
PythonPackageReadmeContentConfigValue,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return PythonPackageReadmeContentConfigValue(workdir=self)
|
|
189
|
+
|
|
190
|
+
def _get_suite_package_workdir_class(self) -> type[FrameworkPackageSuiteWorkdir]:
|
|
338
191
|
from wexample_wex_addon_dev_python.workdir.python_packages_suite_workdir import (
|
|
339
192
|
PythonPackagesSuiteWorkdir,
|
|
340
193
|
)
|
|
341
194
|
|
|
342
195
|
return PythonPackagesSuiteWorkdir
|
|
343
196
|
|
|
344
|
-
def
|
|
345
|
-
|
|
346
|
-
|
|
197
|
+
def _install_dependencies_in_venv(
|
|
198
|
+
self, venv_path: Path, env: str | None = None, force: bool = False
|
|
199
|
+
) -> None:
|
|
200
|
+
from wexample_app.const.env import ENV_NAME_LOCAL
|
|
201
|
+
from wexample_wex_addon_app.helpers.python import (
|
|
202
|
+
python_install_dependencies_in_venv,
|
|
347
203
|
)
|
|
348
204
|
|
|
349
|
-
|
|
205
|
+
suite_workdir = self.get_shallow_suite_workdir()
|
|
206
|
+
toml_file = self.get_app_config_file()
|
|
350
207
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
"""Check if a package is already installed in editable mode at the correct path."""
|
|
358
|
-
import subprocess
|
|
359
|
-
|
|
360
|
-
try:
|
|
361
|
-
result = subprocess.run(
|
|
362
|
-
[".venv/bin/python", "-m", "pip", "show", package_name],
|
|
363
|
-
cwd=app_path,
|
|
364
|
-
capture_output=True,
|
|
365
|
-
text=True,
|
|
366
|
-
timeout=5,
|
|
367
|
-
)
|
|
208
|
+
# Check for suite only in local env.
|
|
209
|
+
if env == ENV_NAME_LOCAL:
|
|
210
|
+
# Package is part of a suite that may have a venv configured.
|
|
211
|
+
if suite_workdir:
|
|
212
|
+
# Get all dependencies from pyproject.toml
|
|
213
|
+
pyproject_toml_dependencies = toml_file.list_dependencies_names()
|
|
368
214
|
|
|
369
|
-
|
|
370
|
-
|
|
215
|
+
# Get all packages from the suite ordered by dependencies (leaf -> trunk)
|
|
216
|
+
suite_packages = suite_workdir.get_ordered_packages()
|
|
217
|
+
suite_package_names = {pkg.get_package_name() for pkg in suite_packages}
|
|
371
218
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
219
|
+
# Collect all suite packages that need to be installed (including transitive dependencies)
|
|
220
|
+
suite_dependencies_ordered = self._collect_suite_dependencies(
|
|
221
|
+
pyproject_toml_dependencies, suite_workdir, suite_package_names
|
|
222
|
+
)
|
|
376
223
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
224
|
+
# External dependencies are those not in the suite
|
|
225
|
+
external_dependencies = [
|
|
226
|
+
dep
|
|
227
|
+
for dep in pyproject_toml_dependencies
|
|
228
|
+
if dep not in suite_package_names
|
|
229
|
+
]
|
|
382
230
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
231
|
+
self.subtitle(
|
|
232
|
+
f"Installing {len(external_dependencies)} external packages",
|
|
233
|
+
indentation=1,
|
|
234
|
+
)
|
|
235
|
+
python_install_dependencies_in_venv(
|
|
236
|
+
venv_path=venv_path, names=external_dependencies
|
|
237
|
+
)
|
|
386
238
|
|
|
387
|
-
|
|
239
|
+
# Install suite packages in editable mode (leaf -> trunk order)
|
|
240
|
+
if suite_dependencies_ordered:
|
|
241
|
+
self.subtitle(
|
|
242
|
+
f"Installing {len(suite_dependencies_ordered)} suite packages in editable mode (leaf -> trunk)",
|
|
243
|
+
indentation=1,
|
|
244
|
+
)
|
|
388
245
|
|
|
389
|
-
|
|
246
|
+
editable_paths = []
|
|
390
247
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
248
|
+
for pkg in suite_dependencies_ordered:
|
|
249
|
+
pkg_path = pkg.get_path()
|
|
250
|
+
pkg_name = pkg.get_package_name()
|
|
394
251
|
|
|
395
|
-
|
|
396
|
-
|
|
252
|
+
if force or not python_is_package_installed_editable_in_venv(
|
|
253
|
+
venv_path=venv_path,
|
|
254
|
+
package_name=pkg_name,
|
|
255
|
+
package_path=pkg_path,
|
|
256
|
+
):
|
|
257
|
+
editable_paths.append(str(pkg_path))
|
|
397
258
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
)
|
|
259
|
+
python_install_dependencies_in_venv(
|
|
260
|
+
venv_path=venv_path,
|
|
261
|
+
names=editable_paths,
|
|
262
|
+
editable=True,
|
|
263
|
+
)
|
|
404
264
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
265
|
+
self.subtitle(
|
|
266
|
+
"Installing dev group dependencies",
|
|
267
|
+
indentation=1,
|
|
268
|
+
)
|
|
269
|
+
python_install_dependencies_in_venv(
|
|
270
|
+
venv_path=venv_path,
|
|
271
|
+
names=self.get_app_config_file().optional_group_array(group="dev"),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
self.subtitle(
|
|
275
|
+
"Installing itself in editable mode",
|
|
276
|
+
indentation=1,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Install itself as editable.
|
|
280
|
+
python_install_dependency_in_venv(
|
|
281
|
+
venv_path=venv_path, name=self.get_path(), editable=True
|
|
282
|
+
)
|
|
409
283
|
|
|
410
|
-
|
|
411
|
-
self.log("pdm.lock is out of date")
|
|
284
|
+
return
|
|
412
285
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
self.success("pdm.lock updated")
|
|
416
|
-
except Exception:
|
|
417
|
-
self.failure("pdm.lock updated")
|
|
286
|
+
# Fallback to parent behaviour
|
|
287
|
+
super()._install_dependencies_in_venv(venv_path=venv_path, env=env, force=force)
|
|
418
288
|
|
|
419
289
|
def _publish(self, force: bool = False) -> None:
|
|
420
290
|
from wexample_filestate_python.common.pipy_gateway import PipyGateway
|
|
@@ -71,94 +71,6 @@ class PythonPackagesSuiteWorkdir(FrameworkPackageSuiteWorkdir):
|
|
|
71
71
|
|
|
72
72
|
return stack if stack and stack[-1].get_package_name() == target else []
|
|
73
73
|
|
|
74
|
-
def build_ordered_dependencies(self) -> list[str]:
|
|
75
|
-
# Build and validate the dependency map, then compute a stable topological order
|
|
76
|
-
return self.topological_order(self.build_dependencies_map())
|
|
77
|
-
|
|
78
|
-
def get_dependents(
|
|
79
|
-
self, package: PythonPackageWorkdir
|
|
80
|
-
) -> list[PythonPackageWorkdir]:
|
|
81
|
-
dependents = []
|
|
82
|
-
for neighbor_package in self.get_packages():
|
|
83
|
-
if neighbor_package.depends_from(package):
|
|
84
|
-
dependents.append(neighbor_package)
|
|
85
|
-
return dependents
|
|
86
|
-
|
|
87
|
-
def get_ordered_packages(self) -> list[PythonPackageWorkdir]:
|
|
88
|
-
"""Return package objects ordered leaves -> trunk."""
|
|
89
|
-
order = self.build_ordered_dependencies()
|
|
90
|
-
by_name = {p.get_package_name(): p for p in self.get_packages()}
|
|
91
|
-
return [by_name[n] for n in order]
|
|
92
|
-
|
|
93
|
-
def packages_validate_internal_dependencies_declarations(self) -> None:
|
|
94
|
-
from wexample_wex_addon_app.exception.dependency_violation_exception import (
|
|
95
|
-
DependencyViolationException,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
dependencies_map = self.build_dependencies_map()
|
|
99
|
-
|
|
100
|
-
self.io.log("Checking packages dependencies consistency...")
|
|
101
|
-
self.io.indentation_up()
|
|
102
|
-
progress = self.io.progress(
|
|
103
|
-
total=len(dependencies_map), print_response=False
|
|
104
|
-
).get_handle()
|
|
105
|
-
|
|
106
|
-
for package_name in dependencies_map:
|
|
107
|
-
package = self.get_package(package_name)
|
|
108
|
-
|
|
109
|
-
for package_name_search in dependencies_map:
|
|
110
|
-
searched_package = self.get_package(package_name_search)
|
|
111
|
-
imports = package.search_imports_in_codebase(searched_package)
|
|
112
|
-
if len(imports) > 0:
|
|
113
|
-
dependencies_stack = self.build_dependencies_stack(
|
|
114
|
-
package, searched_package, dependencies_map
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
if len(dependencies_stack) == 0:
|
|
118
|
-
# Build a readable list of import locations to help debugging
|
|
119
|
-
import_locations = [
|
|
120
|
-
f"{res.item.get_path()}:{res.line}:{res.column}"
|
|
121
|
-
for res in imports
|
|
122
|
-
]
|
|
123
|
-
raise DependencyViolationException(
|
|
124
|
-
package_name=package_name,
|
|
125
|
-
imported_package=package_name_search,
|
|
126
|
-
import_locations=import_locations,
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
progress.advance(label=f"Package {package.get_project_name()}", step=1)
|
|
130
|
-
|
|
131
|
-
self.io.success("Internal dependencies match.")
|
|
132
|
-
self.io.indentation_down()
|
|
133
|
-
|
|
134
|
-
def topological_order(self, dep_map: dict[str, list[str]]) -> list[str]:
|
|
135
|
-
"""Deterministic topological order using graphlib.TopologicalSorter.
|
|
136
|
-
Returns a leaves -> trunk order (dependencies before dependents).
|
|
137
|
-
Raises ValueError on cycles.
|
|
138
|
-
"""
|
|
139
|
-
from graphlib import CycleError, TopologicalSorter
|
|
140
|
-
|
|
141
|
-
# Normalize: include every mentioned node and sort for stable results
|
|
142
|
-
nodes = set(dep_map.keys()) | {d for deps in dep_map.values() for d in deps}
|
|
143
|
-
normalized: dict[str, list[str]] = {
|
|
144
|
-
k: sorted([d for d in dep_map.get(k, []) if d in nodes])
|
|
145
|
-
for k in sorted(nodes)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
ts = TopologicalSorter()
|
|
149
|
-
for k, deps in normalized.items():
|
|
150
|
-
ts.add(k, *deps)
|
|
151
|
-
|
|
152
|
-
try:
|
|
153
|
-
order = list(ts.static_order())
|
|
154
|
-
except CycleError as e:
|
|
155
|
-
# Extract involved nodes if present, otherwise a generic message
|
|
156
|
-
msg = getattr(e, "args", [None])[0] or "Cyclic dependencies detected"
|
|
157
|
-
raise ValueError(str(msg)) from e
|
|
158
|
-
|
|
159
|
-
# Return only local packages (original keys of dep_map)
|
|
160
|
-
return [n for n in order if n in dep_map]
|
|
161
|
-
|
|
162
74
|
def _child_is_package_directory(self, entry: Path) -> bool:
|
|
163
75
|
return entry.is_dir() and (entry / "pyproject.toml").is_file()
|
|
164
76
|
|
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
|
-
from tomlkit import TOMLDocument
|
|
7
6
|
from wexample_app.item.file.iml_file import ImlFile
|
|
8
7
|
from wexample_event.dataclass.event import Event
|
|
9
8
|
from wexample_event.dataclass.listener_record import EventCallback
|
|
@@ -16,8 +15,6 @@ from wexample_filestate_python.const.python_file import (
|
|
|
16
15
|
PYTHON_FILE_EXTENSION,
|
|
17
16
|
PYTHON_FILE_PYTEST_COVERAGE_JSON,
|
|
18
17
|
)
|
|
19
|
-
from wexample_wex_addon_app.helpers.python import python_install_environment
|
|
20
|
-
from wexample_wex_addon_app.item.file.python_app_iml_file import PythonAppImlFile
|
|
21
18
|
from wexample_wex_addon_app.workdir.code_base_workdir import (
|
|
22
19
|
CodeBaseWorkdir,
|
|
23
20
|
)
|
|
@@ -27,6 +24,7 @@ from wexample_wex_addon_dev_python.const.python import (
|
|
|
27
24
|
PYTHON_PYTEST_COV_FORMAT_JSON,
|
|
28
25
|
PYTHON_PYTEST_COV_REPORT_DIR,
|
|
29
26
|
)
|
|
27
|
+
from wexample_wex_addon_dev_python.file.python_app_iml_file import PythonAppImlFile
|
|
30
28
|
|
|
31
29
|
if TYPE_CHECKING:
|
|
32
30
|
from wexample_config.const.types import DictConfig
|
|
@@ -41,23 +39,51 @@ if TYPE_CHECKING:
|
|
|
41
39
|
)
|
|
42
40
|
from wexample_helpers.const.types import StructuredData
|
|
43
41
|
|
|
44
|
-
from wexample_wex_addon_dev_python.file.
|
|
45
|
-
|
|
42
|
+
from wexample_wex_addon_dev_python.file.python_pyproject_toml_file import (
|
|
43
|
+
PythonPyprojectTomlFile,
|
|
46
44
|
)
|
|
47
45
|
|
|
48
46
|
|
|
49
47
|
class PythonWorkdir(CodeBaseWorkdir):
|
|
50
|
-
def app_install(self, env: str | None = None, force: bool = False) ->
|
|
48
|
+
def app_install(self, env: str | None = None, force: bool = False) -> Path:
|
|
49
|
+
from wexample_wex_addon_app.helpers.python import (
|
|
50
|
+
python_ensure_pip_or_fail,
|
|
51
|
+
python_install_environment,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Check if a venv path is somewhere in the config hierarchy.
|
|
55
|
+
venv_path_config = self.search_app_or_suite_runtime_config("python.venv_path")
|
|
56
|
+
|
|
57
|
+
# There is no venv, so create a venv for this project.
|
|
58
|
+
if venv_path_config.is_none():
|
|
59
|
+
venv_path = python_install_environment(path=self.get_path())
|
|
60
|
+
else:
|
|
61
|
+
venv_path = Path(venv_path_config.get_str())
|
|
62
|
+
|
|
63
|
+
self.log(f"Using venv: @path{{{venv_path}}}")
|
|
64
|
+
python_ensure_pip_or_fail(venv_path)
|
|
65
|
+
|
|
66
|
+
self._install_dependencies_in_venv(
|
|
67
|
+
venv_path=venv_path,
|
|
68
|
+
env=env,
|
|
69
|
+
force=force,
|
|
70
|
+
)
|
|
71
|
+
|
|
51
72
|
# Use standard PDM install
|
|
52
|
-
return
|
|
73
|
+
return venv_path
|
|
53
74
|
|
|
54
|
-
def
|
|
55
|
-
from
|
|
75
|
+
def get_app_config_file(self, reload: bool = True) -> PythonPyprojectTomlFile:
|
|
76
|
+
from wexample_wex_addon_dev_python.file.python_pyproject_toml_file import (
|
|
77
|
+
PythonPyprojectTomlFile,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
config_file = self.find_by_type(PythonPyprojectTomlFile)
|
|
81
|
+
# Read once to populate content with file source.
|
|
82
|
+
config_file.read_text(reload=reload)
|
|
83
|
+
return config_file
|
|
56
84
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
dependencies.append(Requirement(dependency).name)
|
|
60
|
-
return dependencies
|
|
85
|
+
def get_dependencies_versions(self) -> dict[str, str]:
|
|
86
|
+
return self.get_app_config_file().get_dependencies_versions()
|
|
61
87
|
|
|
62
88
|
def get_main_code_file_extension(self) -> str:
|
|
63
89
|
return PYTHON_FILE_EXTENSION
|
|
@@ -86,23 +112,6 @@ class PythonWorkdir(CodeBaseWorkdir):
|
|
|
86
112
|
|
|
87
113
|
return string_to_kebab_case(self.get_package_import_name())
|
|
88
114
|
|
|
89
|
-
def get_project_config(self, reload: bool = True) -> TOMLDocument:
|
|
90
|
-
"""
|
|
91
|
-
Fetch the data from the pyproject.toml file.
|
|
92
|
-
"""
|
|
93
|
-
return self.get_project_config_file(reload=reload).read_parsed()
|
|
94
|
-
|
|
95
|
-
def get_project_config_file(self, reload: bool = True) -> PythonPackageTomlFile:
|
|
96
|
-
from wexample_wex_addon_dev_python.file.python_package_toml_file import (
|
|
97
|
-
PythonPackageTomlFile,
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
config_file = self.find_by_name("pyproject.toml")
|
|
101
|
-
assert isinstance(config_file, PythonPackageTomlFile)
|
|
102
|
-
# Read once to populate content with file source.
|
|
103
|
-
config_file.read_text(reload=reload)
|
|
104
|
-
return config_file
|
|
105
|
-
|
|
106
115
|
def get_python_exec_module_command(self, module_name: str) -> list[str]:
|
|
107
116
|
return [self.get_python_path(), "-m", module_name]
|
|
108
117
|
|
|
@@ -151,8 +160,8 @@ class PythonWorkdir(CodeBaseWorkdir):
|
|
|
151
160
|
)
|
|
152
161
|
from wexample_helpers.helpers.array import array_dict_get_by
|
|
153
162
|
|
|
154
|
-
from wexample_wex_addon_dev_python.file.
|
|
155
|
-
|
|
163
|
+
from wexample_wex_addon_dev_python.file.python_pyproject_toml_file import (
|
|
164
|
+
PythonPyprojectTomlFile,
|
|
156
165
|
)
|
|
157
166
|
|
|
158
167
|
raw_value = super().prepare_value(raw_value=raw_value)
|
|
@@ -166,7 +175,6 @@ class PythonWorkdir(CodeBaseWorkdir):
|
|
|
166
175
|
[
|
|
167
176
|
".pdm-python",
|
|
168
177
|
".python-version",
|
|
169
|
-
".venv",
|
|
170
178
|
f"/{PYTHON_FILE_PYTEST_COVERAGE_JSON}",
|
|
171
179
|
]
|
|
172
180
|
)
|
|
@@ -183,34 +191,7 @@ class PythonWorkdir(CodeBaseWorkdir):
|
|
|
183
191
|
],
|
|
184
192
|
},
|
|
185
193
|
{
|
|
186
|
-
"
|
|
187
|
-
"type": DiskItemType.DIRECTORY,
|
|
188
|
-
"should_exist": True,
|
|
189
|
-
},
|
|
190
|
-
# Replaced by pdm
|
|
191
|
-
{
|
|
192
|
-
"name": "requirements.in",
|
|
193
|
-
"type": DiskItemType.FILE,
|
|
194
|
-
"should_exist": False,
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
"name": "requirements.txt",
|
|
198
|
-
"type": DiskItemType.FILE,
|
|
199
|
-
"should_exist": False,
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
"name": "requirements-dev.in",
|
|
203
|
-
"type": DiskItemType.FILE,
|
|
204
|
-
"should_exist": False,
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
"name": "requirements-dev.txt",
|
|
208
|
-
"type": DiskItemType.FILE,
|
|
209
|
-
"should_exist": False,
|
|
210
|
-
},
|
|
211
|
-
# Pdm versions
|
|
212
|
-
{
|
|
213
|
-
"class": PythonPackageTomlFile,
|
|
194
|
+
"class": PythonPyprojectTomlFile,
|
|
214
195
|
"name": "pyproject.toml",
|
|
215
196
|
"type": DiskItemType.FILE,
|
|
216
197
|
"should_exist": True,
|
|
@@ -264,8 +245,8 @@ class PythonWorkdir(CodeBaseWorkdir):
|
|
|
264
245
|
|
|
265
246
|
def save_dependency(self, package_name: str, version: str) -> bool:
|
|
266
247
|
"""Add or update a dependency with strict version."""
|
|
267
|
-
config = self.
|
|
268
|
-
updated = config.add_dependency(
|
|
248
|
+
config = self.get_app_config_file()
|
|
249
|
+
updated = config.add_dependency(package_name=package_name, version=version)
|
|
269
250
|
|
|
270
251
|
if updated:
|
|
271
252
|
config.write_parsed()
|
|
@@ -274,7 +255,7 @@ class PythonWorkdir(CodeBaseWorkdir):
|
|
|
274
255
|
|
|
275
256
|
def save_project_config_file(self, config: StructuredData) -> None:
|
|
276
257
|
"""Save the project configuration to pyproject.toml."""
|
|
277
|
-
config_file = self.
|
|
258
|
+
config_file = self.get_app_config_file()
|
|
278
259
|
config_file.write(config)
|
|
279
260
|
|
|
280
261
|
def test_get_command(
|
|
@@ -327,7 +308,7 @@ class PythonWorkdir(CodeBaseWorkdir):
|
|
|
327
308
|
from packaging.requirements import Requirement
|
|
328
309
|
from packaging.utils import canonicalize_name
|
|
329
310
|
|
|
330
|
-
config_file = self.
|
|
311
|
+
config_file = self.get_app_config_file()
|
|
331
312
|
|
|
332
313
|
# Canonicalize the keys in dependencies_map for consistent matching
|
|
333
314
|
canonical_map = {
|
|
@@ -336,7 +317,7 @@ class PythonWorkdir(CodeBaseWorkdir):
|
|
|
336
317
|
}
|
|
337
318
|
|
|
338
319
|
# Get current dependencies
|
|
339
|
-
current_deps = config_file.
|
|
320
|
+
current_deps = config_file.list_dependencies_names()
|
|
340
321
|
|
|
341
322
|
# Update each dependency if it's in the map
|
|
342
323
|
for dep_spec in current_deps:
|
|
@@ -348,7 +329,9 @@ class PythonWorkdir(CodeBaseWorkdir):
|
|
|
348
329
|
if canonical_name in canonical_map:
|
|
349
330
|
new_version = canonical_map[canonical_name]
|
|
350
331
|
# Use add_dependency which handles removal of old version
|
|
351
|
-
config_file.add_dependency(
|
|
332
|
+
config_file.add_dependency(
|
|
333
|
+
package_name=req.name, version=new_version
|
|
334
|
+
)
|
|
352
335
|
except Exception:
|
|
353
336
|
# Skip unparsable dependencies
|
|
354
337
|
continue
|
|
@@ -524,5 +507,18 @@ class PythonWorkdir(CodeBaseWorkdir):
|
|
|
524
507
|
operation=FileRenameOperation, suffix="post", callback=self._on_test_event
|
|
525
508
|
)
|
|
526
509
|
|
|
510
|
+
def _install_dependencies_in_venv(
|
|
511
|
+
self, venv_path: Path, env: str | None = None, force: bool = False
|
|
512
|
+
) -> None:
|
|
513
|
+
from wexample_wex_addon_app.helpers.python import (
|
|
514
|
+
python_install_dependencies_in_venv,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
toml_file = self.get_app_config_file()
|
|
518
|
+
# Get all dependencies from pyproject.toml
|
|
519
|
+
python_install_dependencies_in_venv(
|
|
520
|
+
venv_path=venv_path, names=toml_file.list_dependencies_names()
|
|
521
|
+
)
|
|
522
|
+
|
|
527
523
|
def _on_test_event(self, event: Event) -> None:
|
|
528
524
|
self.success("A python file has been renamed")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: wexample-wex-addon-dev-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.62
|
|
4
4
|
Summary: Python dev addon for wex
|
|
5
5
|
Author-Email: weeger <contact@wexample.com>
|
|
6
6
|
License: MIT
|
|
@@ -11,13 +11,12 @@ Project-URL: homepage, https://github.com/wexample/python-wex-dev-python
|
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
12
|
Requires-Dist: attrs>=23.1.0
|
|
13
13
|
Requires-Dist: cattrs>=23.1.0
|
|
14
|
-
Requires-Dist: jinja2
|
|
15
14
|
Requires-Dist: networkx
|
|
16
15
|
Requires-Dist: pylint
|
|
17
16
|
Requires-Dist: pyright
|
|
18
|
-
Requires-Dist: wexample-filestate-python==0.0.
|
|
19
|
-
Requires-Dist: wexample-wex-addon-app==0.0.
|
|
20
|
-
Requires-Dist: wexample-wex-core==6.0.
|
|
17
|
+
Requires-Dist: wexample-filestate-python==0.0.57
|
|
18
|
+
Requires-Dist: wexample-wex-addon-app==0.0.54
|
|
19
|
+
Requires-Dist: wexample-wex-core==6.0.66
|
|
21
20
|
Provides-Extra: dev
|
|
22
21
|
Requires-Dist: pytest; extra == "dev"
|
|
23
22
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -25,10 +24,49 @@ Description-Content-Type: text/markdown
|
|
|
25
24
|
|
|
26
25
|
# wexample-wex-addon-dev-python
|
|
27
26
|
|
|
28
|
-
Version: 0.0.
|
|
27
|
+
Version: 0.0.62
|
|
29
28
|
|
|
30
29
|
Python dev addon for wex
|
|
31
30
|
|
|
31
|
+
## Table of Contents
|
|
32
|
+
|
|
33
|
+
- [Status Compatibility](#status-compatibility)
|
|
34
|
+
- [Api Reference](#api-reference)
|
|
35
|
+
- [Tests](#tests)
|
|
36
|
+
- [Code Quality](#code-quality)
|
|
37
|
+
- [Versioning](#versioning)
|
|
38
|
+
- [Changelog](#changelog)
|
|
39
|
+
- [Migration Notes](#migration-notes)
|
|
40
|
+
- [Roadmap](#roadmap)
|
|
41
|
+
- [Security](#security)
|
|
42
|
+
- [Privacy](#privacy)
|
|
43
|
+
- [Support](#support)
|
|
44
|
+
- [Contribution Guidelines](#contribution-guidelines)
|
|
45
|
+
- [Maintainers](#maintainers)
|
|
46
|
+
- [License](#license)
|
|
47
|
+
- [Useful Links](#useful-links)
|
|
48
|
+
- [Suite Integration](#suite-integration)
|
|
49
|
+
- [Compatibility Matrix](#compatibility-matrix)
|
|
50
|
+
- [Dependencies](#dependencies)
|
|
51
|
+
- [Suite Signature](#suite-signature)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## Status & Compatibility
|
|
55
|
+
|
|
56
|
+
**Maturity**: Production-ready
|
|
57
|
+
|
|
58
|
+
**Python Support**: >=3.10
|
|
59
|
+
|
|
60
|
+
**OS Support**: Linux, macOS, Windows
|
|
61
|
+
|
|
62
|
+
**Status**: Actively maintained
|
|
63
|
+
|
|
64
|
+
## API Reference
|
|
65
|
+
|
|
66
|
+
Full API documentation is available in the source code docstrings.
|
|
67
|
+
|
|
68
|
+
Key modules and classes are documented with type hints for better IDE support.
|
|
69
|
+
|
|
32
70
|
## Tests
|
|
33
71
|
|
|
34
72
|
This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
|
|
@@ -115,13 +153,13 @@ Breaking changes are clearly documented with upgrade paths and examples.
|
|
|
115
153
|
|
|
116
154
|
Current limitations and planned features are tracked in the GitHub issues.
|
|
117
155
|
|
|
118
|
-
See the [project roadmap](https://github.com/wexample/python-
|
|
156
|
+
See the [project roadmap](https://github.com/wexample/python-wex_addon_dev_python/issues) for upcoming features and improvements.
|
|
119
157
|
|
|
120
158
|
## Security Policy
|
|
121
159
|
|
|
122
160
|
### Reporting Vulnerabilities
|
|
123
161
|
|
|
124
|
-
If you discover a security vulnerability, please email
|
|
162
|
+
If you discover a security vulnerability, please email contact@wexample.com.
|
|
125
163
|
|
|
126
164
|
**Do not** open public issues for security vulnerabilities.
|
|
127
165
|
|
|
@@ -144,7 +182,7 @@ Community support is available through GitHub Discussions.
|
|
|
144
182
|
|
|
145
183
|
## Contribution Guidelines
|
|
146
184
|
|
|
147
|
-
We welcome contributions to the Wexample suite!
|
|
185
|
+
We welcome contributions to the Wexample suite!
|
|
148
186
|
|
|
149
187
|
### How to Contribute
|
|
150
188
|
|
|
@@ -162,14 +200,16 @@ See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the full list of contributors.
|
|
|
162
200
|
|
|
163
201
|
## License
|
|
164
202
|
|
|
165
|
-
MIT
|
|
203
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
204
|
+
|
|
205
|
+
Free to use in both personal and commercial projects.
|
|
166
206
|
|
|
167
207
|
## Useful Links
|
|
168
208
|
|
|
169
|
-
- **Homepage**: https://github.com/wexample/python-wex-dev-python
|
|
209
|
+
- **Homepage**: https://github.com/wexample/python-wex-addon-dev-python
|
|
170
210
|
- **Documentation**: [docs.wexample.com](https://docs.wexample.com)
|
|
171
|
-
- **Issue Tracker**: https://github.com/wexample/python-wex-dev-python/issues
|
|
172
|
-
- **Discussions**: https://github.com/wexample/python-wex-dev-python/discussions
|
|
211
|
+
- **Issue Tracker**: https://github.com/wexample/python-wex-addon-dev-python/issues
|
|
212
|
+
- **Discussions**: https://github.com/wexample/python-wex-addon-dev-python/discussions
|
|
173
213
|
- **PyPI**: [pypi.org/project/wexample-wex-addon-dev-python](https://pypi.org/project/wexample-wex-addon-dev-python/)
|
|
174
214
|
|
|
175
215
|
## Integration in the Suite
|
|
@@ -182,9 +222,27 @@ The suite includes packages for configuration management, file handling, prompts
|
|
|
182
222
|
|
|
183
223
|
Visit the [Wexample Suite documentation](https://docs.wexample.com) for the complete package ecosystem.
|
|
184
224
|
|
|
225
|
+
## Compatibility Matrix
|
|
226
|
+
|
|
227
|
+
This package is part of the Wexample suite and is compatible with other suite packages.
|
|
228
|
+
|
|
229
|
+
Refer to each package's documentation for specific version compatibility requirements.
|
|
230
|
+
|
|
231
|
+
## Dependencies
|
|
232
|
+
|
|
233
|
+
- attrs: >=23.1.0
|
|
234
|
+
- cattrs: >=23.1.0
|
|
235
|
+
- networkx:
|
|
236
|
+
- pylint:
|
|
237
|
+
- pyright:
|
|
238
|
+
- wexample-filestate-python: ==0.0.57
|
|
239
|
+
- wexample-wex-addon-app: ==0.0.54
|
|
240
|
+
- wexample-wex-core: ==6.0.66
|
|
241
|
+
|
|
242
|
+
|
|
185
243
|
# About us
|
|
186
244
|
|
|
187
|
-
Wexample stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.
|
|
245
|
+
[Wexample](https://wexample.com) stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.
|
|
188
246
|
|
|
189
247
|
This packages suite embodies this spirit. Trusted by professionals and enthusiasts alike, it delivers a consistent, high-quality foundation for modern development — open, elegant, and battle-tested. Its reputation is built on years of collaboration, refinement, and rigorous attention to detail, making it a natural choice for those who demand both robustness and beauty in their tools.
|
|
190
248
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
wexample_wex_addon_dev_python-0.0.
|
|
2
|
-
wexample_wex_addon_dev_python-0.0.
|
|
3
|
-
wexample_wex_addon_dev_python-0.0.
|
|
1
|
+
wexample_wex_addon_dev_python-0.0.62.dist-info/METADATA,sha256=wfg0YuX89h46KwubjV5ys0GaCD4PuplKcQeBaALDRLM,8086
|
|
2
|
+
wexample_wex_addon_dev_python-0.0.62.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
|
|
3
|
+
wexample_wex_addon_dev_python-0.0.62.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
4
|
wexample_wex_addon_dev_python/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
wexample_wex_addon_dev_python/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
wexample_wex_addon_dev_python/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -21,14 +21,15 @@ wexample_wex_addon_dev_python/commands/examples/validate.py,sha256=jHiWkEtETkVSY
|
|
|
21
21
|
wexample_wex_addon_dev_python/commands/release/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
wexample_wex_addon_dev_python/config_value/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
wexample_wex_addon_dev_python/config_value/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
-
wexample_wex_addon_dev_python/config_value/python_package_readme_config_value.py,sha256=
|
|
24
|
+
wexample_wex_addon_dev_python/config_value/python_package_readme_config_value.py,sha256=9L67C6oGwebd8tMSqECmSSpah7nsDTkjEQMNMCWAyp0,1722
|
|
25
25
|
wexample_wex_addon_dev_python/const/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
26
|
wexample_wex_addon_dev_python/const/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
27
|
wexample_wex_addon_dev_python/const/package.py,sha256=oRCPhaazJp5TujxF-35rrIYA4FJsqMqCns8lOFKOLeA,451
|
|
28
28
|
wexample_wex_addon_dev_python/const/python.py,sha256=jxdPt5CnD0dcp4SmobEc_c7XcCkPFfX_lk3SVHsiVpM,203
|
|
29
29
|
wexample_wex_addon_dev_python/file/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
30
|
wexample_wex_addon_dev_python/file/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
-
wexample_wex_addon_dev_python/file/
|
|
31
|
+
wexample_wex_addon_dev_python/file/python_app_iml_file.py,sha256=l6YHEILxSGFjOvYWY20zIyAODjOIfuyHsuCfSMw0jVE,1201
|
|
32
|
+
wexample_wex_addon_dev_python/file/python_pyproject_toml_file.py,sha256=EGWwBy5W7zNf8-q7XqXHKV8Qb6FVFoW3qm6Rtwcn4NY,15911
|
|
32
33
|
wexample_wex_addon_dev_python/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
34
|
wexample_wex_addon_dev_python/middleware/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
35
|
wexample_wex_addon_dev_python/middleware/each_python_file_middleware.py,sha256=UzNEpedCYf31aNONFl0SuSJnoXRzhJhgEiTCaPark6E,2311
|
|
@@ -37,10 +38,9 @@ wexample_wex_addon_dev_python/python_addon_manager.py,sha256=Mmr9F5lOS2gbb8JTB4-
|
|
|
37
38
|
wexample_wex_addon_dev_python/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
39
|
wexample_wex_addon_dev_python/resources/readme_templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
40
|
wexample_wex_addon_dev_python/resources/readme_templates/tests.md.j2,sha256=tKLcDwx7jhQkryXIxWr12AK-hKEaP6Rusu2MrluiABs,1289
|
|
40
|
-
wexample_wex_addon_dev_python/resources/readme_templates/title.md.j2,sha256=U-q_U_WhTTwz3enrht3UTfQ9fwioaKUuJqwYyhBtCiA,64
|
|
41
41
|
wexample_wex_addon_dev_python/workdir/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
42
|
wexample_wex_addon_dev_python/workdir/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
|
-
wexample_wex_addon_dev_python/workdir/python_package_workdir.py,sha256=
|
|
44
|
-
wexample_wex_addon_dev_python/workdir/python_packages_suite_workdir.py,sha256=
|
|
45
|
-
wexample_wex_addon_dev_python/workdir/python_workdir.py,sha256=
|
|
46
|
-
wexample_wex_addon_dev_python-0.0.
|
|
43
|
+
wexample_wex_addon_dev_python/workdir/python_package_workdir.py,sha256=djUfgI1MCn9KOgYCYiwFIpZaGQ28spaiOz6t8gL6FoA,11780
|
|
44
|
+
wexample_wex_addon_dev_python/workdir/python_packages_suite_workdir.py,sha256=ICHHewLpsXiheYzGDEahphBryH98ZVezWzSAy6A5w5I,2874
|
|
45
|
+
wexample_wex_addon_dev_python/workdir/python_workdir.py,sha256=ERpSw5qti1NmHiufOix6bS5lW2vIvclr82SKwlWOhKk,19681
|
|
46
|
+
wexample_wex_addon_dev_python-0.0.62.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|