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.
@@ -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()
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ PYTHON_PYTEST_COV_REPORT_DIR: Path = Path("htmlcov")
6
+ PYTHON_PYTEST_COV_FORMAT_HTML: str = "html"
7
+ PYTHON_PYTEST_COV_FORMAT_JSON: str = "json"
@@ -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
- # Remove existing entries for the same package name before adding the new spec.
26
- new_name = canonicalize_name(Requirement(spec).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
- # Append (or re-append) the new spec if it is not already present verbatim
32
- if spec not in deps:
33
- deps.append(spec)
34
- toml_sort_string_array(deps)
35
- return True
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
- return removed
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
- from wexample_wex_addon_app.workdir.mixin.with_readme_workdir_mixin import (
222
- WithReadmeWorkdirMixin,
223
- )
238
+ pass
224
239
 
225
- readme_file = package.find_by_name(WithReadmeWorkdirMixin.README_FILENAME)
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"] = WithReadmeWorkdirMixin.README_FILENAME
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._optional_group_array("dev")
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._optional_group_array(group)
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%.