wexample-wex-addon-dev-python 0.0.49__py3-none-any.whl → 0.0.60__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 +40 -34
- wexample_wex_addon_dev_python/const/python.py +7 -0
- wexample_wex_addon_dev_python/file/python_package_toml_file.py +98 -56
- wexample_wex_addon_dev_python/resources/readme_templates/tests.md.j2 +48 -0
- wexample_wex_addon_dev_python/workdir/python_package_workdir.py +244 -184
- wexample_wex_addon_dev_python/workdir/python_packages_suite_workdir.py +6 -4
- wexample_wex_addon_dev_python/workdir/python_workdir.py +289 -89
- {wexample_wex_addon_dev_python-0.0.49.dist-info → wexample_wex_addon_dev_python-0.0.60.dist-info}/METADATA +54 -5
- {wexample_wex_addon_dev_python-0.0.49.dist-info → wexample_wex_addon_dev_python-0.0.60.dist-info}/RECORD +11 -9
- {wexample_wex_addon_dev_python-0.0.49.dist-info → wexample_wex_addon_dev_python-0.0.60.dist-info}/WHEEL +1 -1
- {wexample_wex_addon_dev_python-0.0.49.dist-info → wexample_wex_addon_dev_python-0.0.60.dist-info}/entry_points.txt +0 -0
|
@@ -21,41 +21,8 @@ class PythonPackageReadmeContentConfigValue(ReadmeContentConfigValue):
|
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
def get_templates(self) -> list[str] | None:
|
|
24
|
-
# Use TOMLDocument from the workdir
|
|
25
|
-
doc = self.workdir.get_project_config()
|
|
26
|
-
project = doc.get("project", {}) if isinstance(doc, dict) else {}
|
|
27
|
-
|
|
28
|
-
# Extract information
|
|
29
|
-
description = project.get("description", "")
|
|
30
|
-
python_version = project.get("requires-python", "")
|
|
31
|
-
dependencies = project.get("dependencies", [])
|
|
32
|
-
urls = (
|
|
33
|
-
project.get("urls", {}) if isinstance(project.get("urls", {}), dict) else {}
|
|
34
|
-
)
|
|
35
|
-
# Accept both lowercase and capitalized homepage key variants
|
|
36
|
-
homepage = urls.get("homepage") or urls.get("Homepage") or ""
|
|
37
|
-
license_field = project.get("license", {})
|
|
38
|
-
if isinstance(license_field, dict):
|
|
39
|
-
license_info = license_field.get("text", "") or license_field.get(
|
|
40
|
-
"file", ""
|
|
41
|
-
)
|
|
42
|
-
else:
|
|
43
|
-
license_info = str(license_field) if license_field else ""
|
|
44
|
-
|
|
45
|
-
# Format dependencies list
|
|
46
|
-
deps_list = "\n".join([f"- {dep}" for dep in dependencies])
|
|
47
|
-
|
|
48
24
|
# Prepare context for Jinja2 rendering
|
|
49
|
-
context =
|
|
50
|
-
"package_name": self.workdir.get_package_name(),
|
|
51
|
-
"version": self.workdir.get_project_version(),
|
|
52
|
-
"description": description,
|
|
53
|
-
"python_version": python_version,
|
|
54
|
-
"dependencies": dependencies,
|
|
55
|
-
"deps_list": deps_list,
|
|
56
|
-
"homepage": homepage,
|
|
57
|
-
"license_info": license_info,
|
|
58
|
-
}
|
|
25
|
+
context = self._get_template_context()
|
|
59
26
|
|
|
60
27
|
# Define fixed order of README sections
|
|
61
28
|
section_names = [
|
|
@@ -118,6 +85,43 @@ class PythonPackageReadmeContentConfigValue(ReadmeContentConfigValue):
|
|
|
118
85
|
|
|
119
86
|
return [rendered_content]
|
|
120
87
|
|
|
88
|
+
def _get_template_context(self) -> dict:
|
|
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", [])
|
|
97
|
+
urls = (
|
|
98
|
+
project.get("urls", {}) if isinstance(project.get("urls", {}), dict) else {}
|
|
99
|
+
)
|
|
100
|
+
# Accept both lowercase and capitalized homepage key variants
|
|
101
|
+
homepage = urls.get("homepage") or urls.get("Homepage") or ""
|
|
102
|
+
license_field = project.get("license", {})
|
|
103
|
+
if isinstance(license_field, dict):
|
|
104
|
+
license_info = license_field.get("text", "") or license_field.get(
|
|
105
|
+
"file", ""
|
|
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
|
+
|
|
121
125
|
def _render_readme_section(self, section_name: str, context: dict) -> str | None:
|
|
122
126
|
"""
|
|
123
127
|
Render a README section from .md or .md.j2 file with Jinja2 support.
|
|
@@ -137,6 +141,7 @@ class PythonPackageReadmeContentConfigValue(ReadmeContentConfigValue):
|
|
|
137
141
|
Rendered content or None if section file not found
|
|
138
142
|
"""
|
|
139
143
|
from pathlib import Path
|
|
144
|
+
|
|
140
145
|
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
|
|
141
146
|
from wexample_app.const.globals import WORKDIR_SETUP_DIR
|
|
142
147
|
|
|
@@ -201,6 +206,7 @@ class PythonPackageReadmeContentConfigValue(ReadmeContentConfigValue):
|
|
|
201
206
|
True if section file exists, False otherwise
|
|
202
207
|
"""
|
|
203
208
|
from pathlib import Path
|
|
209
|
+
|
|
204
210
|
from wexample_app.const.globals import WORKDIR_SETUP_DIR
|
|
205
211
|
|
|
206
212
|
workdir_path = self.workdir.get_path()
|
|
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING
|
|
|
4
4
|
|
|
5
5
|
from wexample_filestate.item.file.toml_file import TomlFile
|
|
6
6
|
from wexample_helpers.decorator.base_class import base_class
|
|
7
|
+
from wexample_wex_addon_app.const.path import APP_PATH_README
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
10
|
from tomlkit import TOMLDocument
|
|
@@ -22,19 +23,21 @@ class PythonPackageTomlFile(TomlFile):
|
|
|
22
23
|
from wexample_filestate_python.helpers.toml import toml_sort_string_array
|
|
23
24
|
|
|
24
25
|
deps = self._get_deps_array(optional=optional, group=group)
|
|
25
|
-
|
|
26
|
-
new_name = canonicalize_name(
|
|
27
|
-
removed = self.remove_dependency_by_name(
|
|
28
|
-
new_name, optional=optional, group=group
|
|
29
|
-
)
|
|
26
|
+
new_req = Requirement(spec)
|
|
27
|
+
new_name = canonicalize_name(new_req.name)
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
old_spec = None
|
|
30
|
+
for dep in deps:
|
|
31
|
+
if canonicalize_name(Requirement(dep).name) == new_name:
|
|
32
|
+
old_spec = dep
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
self.remove_dependency_by_name(new_name, optional=optional, group=group)
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
deps.append(spec)
|
|
38
|
+
toml_sort_string_array(deps)
|
|
39
|
+
|
|
40
|
+
return old_spec != spec
|
|
38
41
|
|
|
39
42
|
def dumps(self, content: TOMLDocument | dict | None = None) -> str:
|
|
40
43
|
"""Serialize a TOMLDocument (preferred) or a plain dict to TOML.
|
|
@@ -54,6 +57,7 @@ class PythonPackageTomlFile(TomlFile):
|
|
|
54
57
|
self._enforce_project_metadata(content, project_name, project_version)
|
|
55
58
|
self._normalize_dependencies(content)
|
|
56
59
|
self._ensure_dev_dependencies(content)
|
|
60
|
+
self._enforce_pytest_coverage_config(content, import_name)
|
|
57
61
|
self._reorder_toml_sections(content)
|
|
58
62
|
|
|
59
63
|
result = dumps(content)
|
|
@@ -96,6 +100,19 @@ class PythonPackageTomlFile(TomlFile):
|
|
|
96
100
|
continue
|
|
97
101
|
return names
|
|
98
102
|
|
|
103
|
+
def optional_group_array(self, group: str):
|
|
104
|
+
"""Ensure and return project.optional-dependencies[group] as multi-line array."""
|
|
105
|
+
from wexample_filestate_python.helpers.toml import (
|
|
106
|
+
toml_ensure_array,
|
|
107
|
+
toml_ensure_table,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
project = self._project_table()
|
|
111
|
+
opt, _ = toml_ensure_table(project, ["optional-dependencies"])
|
|
112
|
+
arr, _ = toml_ensure_array(opt, group)
|
|
113
|
+
arr.multiline(True)
|
|
114
|
+
return arr
|
|
115
|
+
|
|
99
116
|
def remove_dependency_by_name(
|
|
100
117
|
self, package_name: str, optional: bool = False, group: str = "dev"
|
|
101
118
|
) -> bool:
|
|
@@ -218,20 +235,55 @@ class PythonPackageTomlFile(TomlFile):
|
|
|
218
235
|
|
|
219
236
|
# Add README if it exists
|
|
220
237
|
if package:
|
|
221
|
-
|
|
222
|
-
WithReadmeWorkdirMixin,
|
|
223
|
-
)
|
|
238
|
+
pass
|
|
224
239
|
|
|
225
|
-
readme_file = package.find_by_name(
|
|
240
|
+
readme_file = package.find_by_name(APP_PATH_README)
|
|
226
241
|
if readme_file:
|
|
227
242
|
readme_tbl, _ = toml_ensure_table(project_tbl, ["readme"])
|
|
228
|
-
readme_tbl["file"] =
|
|
243
|
+
readme_tbl["file"] = str(APP_PATH_README)
|
|
229
244
|
readme_tbl["content-type"] = "text/markdown"
|
|
230
245
|
|
|
231
246
|
# Add MIT license
|
|
232
247
|
license_tbl, _ = toml_ensure_table(project_tbl, ["license"])
|
|
233
248
|
license_tbl["text"] = "MIT"
|
|
234
249
|
|
|
250
|
+
def _enforce_pytest_coverage_config(
|
|
251
|
+
self, content: dict, import_name: str | None
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Add pytest and coverage configuration to limit coverage to the package only."""
|
|
254
|
+
if not import_name:
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
from wexample_filestate_python.helpers.toml import toml_ensure_table
|
|
258
|
+
|
|
259
|
+
tool_tbl, _ = toml_ensure_table(content, ["tool"])
|
|
260
|
+
|
|
261
|
+
# Add pytest configuration
|
|
262
|
+
pytest_tbl, _ = toml_ensure_table(tool_tbl, ["pytest", "ini_options"])
|
|
263
|
+
pytest_tbl["testpaths"] = ["tests"]
|
|
264
|
+
pytest_tbl["pythonpath"] = ["src"]
|
|
265
|
+
|
|
266
|
+
# Add coverage.run configuration to limit source to the package
|
|
267
|
+
coverage_run_tbl, _ = toml_ensure_table(tool_tbl, ["coverage", "run"])
|
|
268
|
+
coverage_run_tbl["source"] = [import_name]
|
|
269
|
+
coverage_run_tbl["omit"] = [
|
|
270
|
+
"*/tests/*",
|
|
271
|
+
"*/.venv/*",
|
|
272
|
+
"*/venv/*",
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
# Add coverage.report configuration
|
|
276
|
+
coverage_report_tbl, _ = toml_ensure_table(tool_tbl, ["coverage", "report"])
|
|
277
|
+
coverage_report_tbl["exclude_lines"] = [
|
|
278
|
+
"pragma: no cover",
|
|
279
|
+
"def __repr__",
|
|
280
|
+
"raise AssertionError",
|
|
281
|
+
"raise NotImplementedError",
|
|
282
|
+
"if __name__ == .__main__.:",
|
|
283
|
+
"if TYPE_CHECKING:",
|
|
284
|
+
"@abstractmethod",
|
|
285
|
+
]
|
|
286
|
+
|
|
235
287
|
def _ensure_dev_dependencies(self, content: dict) -> None:
|
|
236
288
|
from wexample_filestate_python.helpers.package import package_normalize_name
|
|
237
289
|
from wexample_filestate_python.helpers.toml import (
|
|
@@ -239,7 +291,7 @@ class PythonPackageTomlFile(TomlFile):
|
|
|
239
291
|
toml_sort_string_array,
|
|
240
292
|
)
|
|
241
293
|
|
|
242
|
-
dev_arr = self.
|
|
294
|
+
dev_arr = self.optional_group_array("dev")
|
|
243
295
|
deps_arr = self._dependencies_array()
|
|
244
296
|
|
|
245
297
|
runtime_pkgs = {
|
|
@@ -257,9 +309,7 @@ class PythonPackageTomlFile(TomlFile):
|
|
|
257
309
|
def _get_deps_array(self, optional: bool = False, group: str = "dev"):
|
|
258
310
|
"""Return TOML array for runtime deps or optional group (multiline)."""
|
|
259
311
|
return (
|
|
260
|
-
self.
|
|
261
|
-
if optional
|
|
262
|
-
else self._dependencies_array()
|
|
312
|
+
self.optional_group_array(group) if optional else self._dependencies_array()
|
|
263
313
|
)
|
|
264
314
|
|
|
265
315
|
def _normalize_dependencies(self, content: dict) -> None:
|
|
@@ -268,6 +318,7 @@ class PythonPackageTomlFile(TomlFile):
|
|
|
268
318
|
toml_get_string_value,
|
|
269
319
|
toml_sort_string_array,
|
|
270
320
|
)
|
|
321
|
+
|
|
271
322
|
from wexample_wex_addon_dev_python.const.package import (
|
|
272
323
|
RUNTIME_DEPENDENCY_REMOVE_NAMES,
|
|
273
324
|
)
|
|
@@ -275,9 +326,24 @@ class PythonPackageTomlFile(TomlFile):
|
|
|
275
326
|
deps_arr = self._dependencies_array()
|
|
276
327
|
toml_sort_string_array(deps_arr)
|
|
277
328
|
|
|
329
|
+
# Read the keep list from [tool.filestate].keep
|
|
330
|
+
keep_packages: set[str] = set()
|
|
331
|
+
if "tool" in content and isinstance(content["tool"], dict):
|
|
332
|
+
tool_tbl = content["tool"]
|
|
333
|
+
if "filestate" in tool_tbl and isinstance(tool_tbl["filestate"], dict):
|
|
334
|
+
filestate_tbl = tool_tbl["filestate"]
|
|
335
|
+
if "keep" in filestate_tbl and isinstance(filestate_tbl["keep"], list):
|
|
336
|
+
keep_packages = {
|
|
337
|
+
package_normalize_name(str(pkg))
|
|
338
|
+
for pkg in filestate_tbl["keep"]
|
|
339
|
+
}
|
|
340
|
+
|
|
278
341
|
# filter unwanted deps
|
|
279
342
|
def _should_remove(item: object) -> bool:
|
|
280
343
|
name = package_normalize_name(toml_get_string_value(item))
|
|
344
|
+
# Don't remove if in keep list
|
|
345
|
+
if name in keep_packages:
|
|
346
|
+
return False
|
|
281
347
|
return name in RUNTIME_DEPENDENCY_REMOVE_NAMES or (
|
|
282
348
|
name == "typing-extensions"
|
|
283
349
|
)
|
|
@@ -287,29 +353,6 @@ class PythonPackageTomlFile(TomlFile):
|
|
|
287
353
|
deps_arr.extend(filtered)
|
|
288
354
|
toml_sort_string_array(deps_arr)
|
|
289
355
|
|
|
290
|
-
# normalize attrs/cattrs
|
|
291
|
-
normalized = []
|
|
292
|
-
for it in list(deps_arr):
|
|
293
|
-
base = package_normalize_name(toml_get_string_value(it).strip())
|
|
294
|
-
if base == "attrs":
|
|
295
|
-
normalized.append("attrs>=23.1.0")
|
|
296
|
-
elif base == "cattrs":
|
|
297
|
-
normalized.append("cattrs>=23.1.0")
|
|
298
|
-
else:
|
|
299
|
-
normalized.append(it)
|
|
300
|
-
if normalized:
|
|
301
|
-
deps_arr.clear()
|
|
302
|
-
deps_arr.extend(normalized)
|
|
303
|
-
toml_sort_string_array(deps_arr)
|
|
304
|
-
|
|
305
|
-
# ensure they are present
|
|
306
|
-
names = {package_normalize_name(toml_get_string_value(it)) for it in deps_arr}
|
|
307
|
-
if "attrs" not in names:
|
|
308
|
-
deps_arr.append("attrs>=23.1.0")
|
|
309
|
-
if "cattrs" not in names:
|
|
310
|
-
deps_arr.append("cattrs>=23.1.0")
|
|
311
|
-
toml_sort_string_array(deps_arr)
|
|
312
|
-
|
|
313
356
|
def _normalize_toml_formatting(self, content: str) -> str:
|
|
314
357
|
"""Normalize TOML formatting:
|
|
315
358
|
- No empty lines at the beginning
|
|
@@ -329,19 +372,6 @@ class PythonPackageTomlFile(TomlFile):
|
|
|
329
372
|
|
|
330
373
|
return content
|
|
331
374
|
|
|
332
|
-
def _optional_group_array(self, group: str):
|
|
333
|
-
"""Ensure and return project.optional-dependencies[group] as multi-line array."""
|
|
334
|
-
from wexample_filestate_python.helpers.toml import (
|
|
335
|
-
toml_ensure_array,
|
|
336
|
-
toml_ensure_table,
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
project = self._project_table()
|
|
340
|
-
opt, _ = toml_ensure_table(project, ["optional-dependencies"])
|
|
341
|
-
arr, _ = toml_ensure_array(opt, group)
|
|
342
|
-
arr.multiline(True)
|
|
343
|
-
return arr
|
|
344
|
-
|
|
345
375
|
def _project_table(self):
|
|
346
376
|
"""Ensure and return the [project] table."""
|
|
347
377
|
from wexample_filestate_python.helpers.toml import toml_ensure_table
|
|
@@ -391,9 +421,21 @@ class PythonPackageTomlFile(TomlFile):
|
|
|
391
421
|
"optional-dependencies",
|
|
392
422
|
]
|
|
393
423
|
|
|
424
|
+
# Define the desired order for keys within [tool]
|
|
425
|
+
tool_key_order = [
|
|
426
|
+
"setuptools",
|
|
427
|
+
"pdm",
|
|
428
|
+
"pytest",
|
|
429
|
+
"coverage",
|
|
430
|
+
]
|
|
431
|
+
|
|
394
432
|
# Reorder top-level sections
|
|
395
433
|
self._reorder_dict_keys(content, section_order)
|
|
396
434
|
|
|
397
435
|
# Reorder keys within [project] if it exists
|
|
398
436
|
if "project" in content:
|
|
399
437
|
self._reorder_dict_keys(content["project"], project_key_order)
|
|
438
|
+
|
|
439
|
+
# Reorder keys within [tool] if it exists
|
|
440
|
+
if "tool" in content:
|
|
441
|
+
self._reorder_dict_keys(content["tool"], tool_key_order)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
## Tests
|
|
2
|
+
|
|
3
|
+
This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
|
|
4
|
+
|
|
5
|
+
### Installation
|
|
6
|
+
|
|
7
|
+
First, install the required testing dependencies:
|
|
8
|
+
```bash
|
|
9
|
+
.venv/bin/python -m pip install pytest pytest-cov
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### Basic Usage
|
|
13
|
+
|
|
14
|
+
Run all tests with coverage:
|
|
15
|
+
```bash
|
|
16
|
+
.venv/bin/python -m pytest --cov --cov-report=html
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Common Commands
|
|
20
|
+
```bash
|
|
21
|
+
# Run tests with coverage for a specific module
|
|
22
|
+
.venv/bin/python -m pytest --cov=your_module
|
|
23
|
+
|
|
24
|
+
# Show which lines are not covered
|
|
25
|
+
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
|
|
26
|
+
|
|
27
|
+
# Generate an HTML coverage report
|
|
28
|
+
.venv/bin/python -m pytest --cov=your_module --cov-report=html
|
|
29
|
+
|
|
30
|
+
# Combine terminal and HTML reports
|
|
31
|
+
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
|
|
32
|
+
|
|
33
|
+
# Run specific test file with coverage
|
|
34
|
+
.venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Viewing HTML Reports
|
|
38
|
+
|
|
39
|
+
After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.
|
|
40
|
+
|
|
41
|
+
### Coverage Threshold
|
|
42
|
+
|
|
43
|
+
To enforce a minimum coverage percentage:
|
|
44
|
+
```bash
|
|
45
|
+
.venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This will cause the test suite to fail if coverage drops below 80%.
|