wexample-wex-addon-dev-python 0.0.53__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.
@@ -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:
@@ -276,9 +326,24 @@ class PythonPackageTomlFile(TomlFile):
276
326
  deps_arr = self._dependencies_array()
277
327
  toml_sort_string_array(deps_arr)
278
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
+
279
341
  # filter unwanted deps
280
342
  def _should_remove(item: object) -> bool:
281
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
282
347
  return name in RUNTIME_DEPENDENCY_REMOVE_NAMES or (
283
348
  name == "typing-extensions"
284
349
  )
@@ -288,29 +353,6 @@ class PythonPackageTomlFile(TomlFile):
288
353
  deps_arr.extend(filtered)
289
354
  toml_sort_string_array(deps_arr)
290
355
 
291
- # normalize attrs/cattrs
292
- normalized = []
293
- for it in list(deps_arr):
294
- base = package_normalize_name(toml_get_string_value(it).strip())
295
- if base == "attrs":
296
- normalized.append("attrs>=23.1.0")
297
- elif base == "cattrs":
298
- normalized.append("cattrs>=23.1.0")
299
- else:
300
- normalized.append(it)
301
- if normalized:
302
- deps_arr.clear()
303
- deps_arr.extend(normalized)
304
- toml_sort_string_array(deps_arr)
305
-
306
- # ensure they are present
307
- names = {package_normalize_name(toml_get_string_value(it)) for it in deps_arr}
308
- if "attrs" not in names:
309
- deps_arr.append("attrs>=23.1.0")
310
- if "cattrs" not in names:
311
- deps_arr.append("cattrs>=23.1.0")
312
- toml_sort_string_array(deps_arr)
313
-
314
356
  def _normalize_toml_formatting(self, content: str) -> str:
315
357
  """Normalize TOML formatting:
316
358
  - No empty lines at the beginning
@@ -330,19 +372,6 @@ class PythonPackageTomlFile(TomlFile):
330
372
 
331
373
  return content
332
374
 
333
- def _optional_group_array(self, group: str):
334
- """Ensure and return project.optional-dependencies[group] as multi-line array."""
335
- from wexample_filestate_python.helpers.toml import (
336
- toml_ensure_array,
337
- toml_ensure_table,
338
- )
339
-
340
- project = self._project_table()
341
- opt, _ = toml_ensure_table(project, ["optional-dependencies"])
342
- arr, _ = toml_ensure_array(opt, group)
343
- arr.multiline(True)
344
- return arr
345
-
346
375
  def _project_table(self):
347
376
  """Ensure and return the [project] table."""
348
377
  from wexample_filestate_python.helpers.toml import toml_ensure_table
@@ -392,9 +421,21 @@ class PythonPackageTomlFile(TomlFile):
392
421
  "optional-dependencies",
393
422
  ]
394
423
 
424
+ # Define the desired order for keys within [tool]
425
+ tool_key_order = [
426
+ "setuptools",
427
+ "pdm",
428
+ "pytest",
429
+ "coverage",
430
+ ]
431
+
395
432
  # Reorder top-level sections
396
433
  self._reorder_dict_keys(content, section_order)
397
434
 
398
435
  # Reorder keys within [project] if it exists
399
436
  if "project" in content:
400
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)
@@ -13,7 +13,7 @@ First, install the required testing dependencies:
13
13
 
14
14
  Run all tests with coverage:
15
15
  ```bash
16
- .venv/bin/python -m pytest --cov
16
+ .venv/bin/python -m pytest --cov --cov-report=html
17
17
  ```
18
18
 
19
19
  ### Common Commands
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ from wexample_filestate.const.disk import DiskItemType
6
+
5
7
  from wexample_wex_addon_dev_python.workdir.python_workdir import PythonWorkdir
6
8
 
7
9
  if TYPE_CHECKING:
@@ -18,88 +20,17 @@ if TYPE_CHECKING:
18
20
  class PythonPackageWorkdir(PythonWorkdir):
19
21
  _project_info_cache = None
20
22
 
21
- def _get_children_package_workdir_class(self) -> type[FrameworkPackageSuiteWorkdir]:
22
- from wexample_wex_addon_dev_python.workdir.python_packages_suite_workdir import (
23
- PythonPackagesSuiteWorkdir,
24
- )
25
-
26
- return PythonPackagesSuiteWorkdir
27
-
28
- def depends_from(self, package: PythonPackageWorkdir) -> bool:
29
- for dependence_name in self.get_dependencies():
30
- if package.get_package_name() == dependence_name:
31
- return True
32
- return False
33
-
34
- def prepare_value(self, raw_value: DictConfig | None = None) -> DictConfig:
35
- from wexample_helpers.helpers.array import array_dict_get_by
36
-
37
- raw_value = super().prepare_value(raw_value=raw_value)
38
-
39
- # Retrieve the '.gitignore' configuration or create it if it doesn't exist
40
- config_gitignore = array_dict_get_by(
41
- "name", ".gitignore", raw_value["children"]
42
- )
43
- if config_gitignore is not None:
44
- generic_gitignore_rules = {
45
- "Python artifacts": [
46
- "*.egg-info",
47
- "__pycache__/",
48
- "*.py[cod]",
49
- "*.pyo",
50
- ],
51
- "Build directories": [
52
- "/build/",
53
- "/dist/",
54
- "/pip-wheel-metadata/",
55
- ],
56
- "Virtual environments": [
57
- ".env",
58
- ".venv",
59
- "venv/",
60
- ],
61
- "Test and coverage artifacts": [
62
- ".tox/",
63
- ".mypy_cache/",
64
- "pytest_cache/",
65
- ".coverage",
66
- "htmlcov/",
67
- ],
68
- "Editor and IDE settings": [
69
- ".vscode/",
70
- ".idea/",
71
- "*.swp",
72
- "*~",
73
- ],
74
- }
75
-
76
- should_contain_lines = config_gitignore.setdefault(
77
- "should_contain_lines", []
78
- )
79
- if not isinstance(should_contain_lines, list):
80
- raise ValueError("'should_contain_lines' must be a list")
81
-
82
- for category, rules in generic_gitignore_rules.items():
83
- category_header = f"# {category}"
84
- if category_header not in should_contain_lines:
85
- should_contain_lines.append(category_header)
86
-
87
- for rule in rules:
88
- if rule not in should_contain_lines:
89
- should_contain_lines.append(rule)
90
-
91
- return raw_value
92
-
93
23
  def app_install(self, env: str | None = None, force: bool = False) -> bool:
94
24
  from wexample_app.const.env import ENV_NAME_LOCAL
95
25
  from wexample_helpers.helpers.shell import shell_run
96
26
 
27
+ # In local env, installs packages using pip.
97
28
  if env == ENV_NAME_LOCAL:
29
+ toml_file = self.get_project_config_file()
98
30
  # Get all dependencies from pyproject.toml
99
- pyproject_toml_dependencies = (
100
- self.get_project_config_file().list_dependency_names()
101
- )
102
- suite_workdir = self.get_suite_workdir()
31
+ pyproject_toml_dependencies = toml_file.list_dependency_names()
32
+
33
+ suite_workdir = self.get_shallow_suite_workdir()
103
34
 
104
35
  # Ensure venv is created and configured
105
36
  app_path = self.get_path()
@@ -112,7 +43,7 @@ class PythonPackageWorkdir(PythonWorkdir):
112
43
  if not venv_is_valid:
113
44
  # Remove corrupted/empty venv if it exists
114
45
  if venv_path.exists():
115
- self.io.log(f"Removing invalid venv at {venv_path}", indentation=1)
46
+ self.log(f"Removing invalid venv at {venv_path}", indentation=1)
116
47
  import shutil
117
48
 
118
49
  shutil.rmtree(venv_path)
@@ -143,6 +74,7 @@ class PythonPackageWorkdir(PythonWorkdir):
143
74
  inherit_stdio=True,
144
75
  )
145
76
 
77
+ # The package is a part of a workdir, so we install manually individual package.
146
78
  if suite_workdir:
147
79
  # Get all packages from the suite ordered by dependencies (leaf -> trunk)
148
80
  suite_packages = suite_workdir.get_ordered_packages()
@@ -162,12 +94,12 @@ class PythonPackageWorkdir(PythonWorkdir):
162
94
 
163
95
  # Install external packages first (normal install)
164
96
  if external_dependencies:
165
- self.io.subtitle(
97
+ self.subtitle(
166
98
  f"Installing {len(external_dependencies)} external packages",
167
99
  indentation=1,
168
100
  )
169
101
  for dep in external_dependencies:
170
- self.io.log(f"Installing {dep}", indentation=2)
102
+ self.log(f"Installing {dep}", indentation=2)
171
103
  shell_run(
172
104
  cmd=[
173
105
  ".venv/bin/python",
@@ -182,7 +114,7 @@ class PythonPackageWorkdir(PythonWorkdir):
182
114
 
183
115
  # Install suite packages in editable mode (leaf -> trunk order)
184
116
  if suite_dependencies_ordered:
185
- self.io.subtitle(
117
+ self.subtitle(
186
118
  f"Installing {len(suite_dependencies_ordered)} suite packages in editable mode (leaf -> trunk)",
187
119
  indentation=1,
188
120
  )
@@ -194,13 +126,13 @@ class PythonPackageWorkdir(PythonWorkdir):
194
126
  if not force and self._is_package_installed_editable(
195
127
  app_path, package_name, package_path
196
128
  ):
197
- self.io.log(
129
+ self.log(
198
130
  f"Skipping {package_name} (already installed in editable mode)",
199
131
  indentation=2,
200
132
  )
201
133
  continue
202
134
 
203
- self.io.log(f"Installing {package_name}", indentation=2)
135
+ self.log(f"Installing {package_name}", indentation=2)
204
136
  shell_run(
205
137
  cmd=[
206
138
  ".venv/bin/python",
@@ -213,6 +145,26 @@ class PythonPackageWorkdir(PythonWorkdir):
213
145
  cwd=app_path,
214
146
  inherit_stdio=True,
215
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
+
216
168
  return True
217
169
 
218
170
  # For non-local environments, use standard PDM install
@@ -221,49 +173,125 @@ class PythonPackageWorkdir(PythonWorkdir):
221
173
  force=force,
222
174
  )
223
175
 
224
- def _is_package_installed_editable(
225
- self,
226
- app_path,
227
- package_name: str,
228
- package_path,
229
- ) -> bool:
230
- """Check if a package is already installed in editable mode at the correct path."""
231
- import subprocess
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
232
181
 
233
- try:
234
- result = subprocess.run(
235
- [".venv/bin/python", "-m", "pip", "show", package_name],
236
- cwd=app_path,
237
- capture_output=True,
238
- text=True,
239
- timeout=5,
182
+ def prepare_value(self, raw_value: DictConfig | None = None) -> DictConfig:
183
+ from wexample_helpers.helpers.array import array_dict_get_by
184
+
185
+ raw_value = super().prepare_value(raw_value=raw_value)
186
+ children = raw_value.get("children")
187
+
188
+ children.append(
189
+ {
190
+ "name": "examples",
191
+ "type": DiskItemType.DIRECTORY,
192
+ "should_exist": True,
193
+ "children": [
194
+ {
195
+ "name": "__main__.py",
196
+ "type": DiskItemType.FILE,
197
+ "should_exist": True,
198
+ },
199
+ ],
200
+ },
201
+ )
202
+
203
+ # Retrieve the '.gitignore' configuration or create it if it doesn't exist
204
+ config_gitignore = array_dict_get_by("name", ".gitignore", children)
205
+ if config_gitignore is not None:
206
+ generic_gitignore_rules = {
207
+ "Python artifacts": [
208
+ "*.egg-info",
209
+ "__pycache__/",
210
+ "*.py[cod]",
211
+ "*.pyo",
212
+ ],
213
+ "Build directories": [
214
+ "/build/",
215
+ "/dist/",
216
+ "/pip-wheel-metadata/",
217
+ ],
218
+ "Virtual environments": [
219
+ ".env",
220
+ ".venv",
221
+ "venv/",
222
+ ],
223
+ "Test and coverage artifacts": [
224
+ ".tox/",
225
+ ".mypy_cache/",
226
+ "pytest_cache/",
227
+ ".coverage",
228
+ "htmlcov/",
229
+ ],
230
+ "Editor and IDE settings": [
231
+ ".vscode/",
232
+ ".idea/",
233
+ "*.swp",
234
+ "*~",
235
+ ],
236
+ }
237
+
238
+ should_contain_lines = config_gitignore.setdefault(
239
+ "should_contain_lines", []
240
240
  )
241
+ if not isinstance(should_contain_lines, list):
242
+ raise ValueError("'should_contain_lines' must be a list")
241
243
 
242
- if result.returncode != 0:
243
- return False
244
+ for category, rules in generic_gitignore_rules.items():
245
+ category_header = f"# {category}"
246
+ if category_header not in should_contain_lines:
247
+ should_contain_lines.append(category_header)
244
248
 
245
- # Parse pip show output
246
- output_lines = result.stdout.strip().split("\n")
247
- location = None
248
- editable_location = None
249
+ for rule in rules:
250
+ if rule not in should_contain_lines:
251
+ should_contain_lines.append(rule)
249
252
 
250
- for line in output_lines:
251
- if line.startswith("Location:"):
252
- location = line.split(":", 1)[1].strip()
253
- elif line.startswith("Editable project location:"):
254
- editable_location = line.split(":", 1)[1].strip()
253
+ return raw_value
255
254
 
256
- # Check if installed in editable mode at the correct path
257
- if editable_location:
258
- from pathlib import Path
255
+ def search_imports_in_codebase(
256
+ self, searched_package: PythonPackageWorkdir
257
+ ) -> list[SearchResult]:
258
+ """Find import statements that reference the given package.
259
259
 
260
- return Path(editable_location).resolve() == Path(package_path).resolve()
260
+ Supports common Python forms:
261
+ - from <pkg>(.<sub>)* import ...
262
+ - import <pkg>(.<sub>)* [as alias]
261
263
 
262
- return False
264
+ Returns a list of SearchResult with file, line and column for each match.
265
+ """
266
+ import re
263
267
 
264
- except Exception:
265
- # If any error occurs, assume not installed
266
- return False
268
+ pkg = searched_package.get_package_import_name()
269
+ pattern = (
270
+ rf"(?m)^\s*(?:"
271
+ rf"from\s+{re.escape(pkg)}(?:\.[\w\.]+)?\s+import\s+"
272
+ rf"|import\s+{re.escape(pkg)}(?:\.[\w\.]+)?(?:\s+as\s+\w+)?\b"
273
+ rf")"
274
+ )
275
+ return self.search_in_codebase(pattern, regex=True, flags=re.MULTILINE)
276
+
277
+ def search_in_codebase(
278
+ self, string: str, *, regex: bool = False, flags: int = 0
279
+ ) -> list[SearchResult]:
280
+ from wexample_filestate.utils.search_result import SearchResult
281
+ from wexample_filestate_python.file.python_file import PythonFile
282
+
283
+ found = []
284
+
285
+ def _search(item: PythonFile) -> None:
286
+ found.extend(
287
+ SearchResult.create_for_all_matches(
288
+ string, item, regex=regex, flags=flags
289
+ )
290
+ )
291
+
292
+ self.for_each_child_of_type_recursive(callback=_search, class_type=PythonFile)
293
+
294
+ return found
267
295
 
268
296
  def _collect_suite_dependencies(
269
297
  self,
@@ -306,6 +334,88 @@ class PythonPackageWorkdir(PythonWorkdir):
306
334
 
307
335
  return suite_deps_ordered
308
336
 
337
+ def _get_children_package_workdir_class(self) -> type[FrameworkPackageSuiteWorkdir]:
338
+ from wexample_wex_addon_dev_python.workdir.python_packages_suite_workdir import (
339
+ PythonPackagesSuiteWorkdir,
340
+ )
341
+
342
+ return PythonPackagesSuiteWorkdir
343
+
344
+ def _get_readme_content(self) -> ReadmeContentConfigValue | None:
345
+ from wexample_wex_addon_dev_python.config_value.python_package_readme_config_value import (
346
+ PythonPackageReadmeContentConfigValue,
347
+ )
348
+
349
+ return PythonPackageReadmeContentConfigValue(workdir=self)
350
+
351
+ def _is_package_installed_editable(
352
+ self,
353
+ app_path,
354
+ package_name: str,
355
+ package_path,
356
+ ) -> bool:
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
+ )
368
+
369
+ if result.returncode != 0:
370
+ return False
371
+
372
+ # Parse pip show output
373
+ output_lines = result.stdout.strip().split("\n")
374
+ location = None
375
+ editable_location = None
376
+
377
+ for line in output_lines:
378
+ if line.startswith("Location:"):
379
+ location = line.split(":", 1)[1].strip()
380
+ elif line.startswith("Editable project location:"):
381
+ editable_location = line.split(":", 1)[1].strip()
382
+
383
+ # Check if installed in editable mode at the correct path
384
+ if editable_location:
385
+ from pathlib import Path
386
+
387
+ return Path(editable_location).resolve() == Path(package_path).resolve()
388
+
389
+ return False
390
+
391
+ except Exception:
392
+ # If any error occurs, assume not installed
393
+ return False
394
+
395
+ def _pdm_run_command(self, command: list[str]) -> None:
396
+ from wexample_helpers.helpers.shell import shell_run
397
+
398
+ # Install dev group
399
+ shell_run(
400
+ cmd=["pdm"] + command,
401
+ cwd=self.get_path(),
402
+ inherit_stdio=True,
403
+ )
404
+
405
+ def _pdm_update_lock_if_needed(self) -> None:
406
+ try:
407
+ self._pdm_run_command(command=["lock", "--check"])
408
+ self.log("pdm.lock is up to date")
409
+
410
+ except Exception:
411
+ self.log("pdm.lock is out of date")
412
+
413
+ try:
414
+ self._pdm_run_command(command=["lock"])
415
+ self.success("pdm.lock updated")
416
+ except Exception:
417
+ self.failure("pdm.lock updated")
418
+
309
419
  def _publish(self, force: bool = False) -> None:
310
420
  from wexample_filestate_python.common.pipy_gateway import PipyGateway
311
421
  from wexample_helpers.helpers.shell import shell_run
@@ -331,51 +441,3 @@ class PythonPackageWorkdir(PythonWorkdir):
331
441
  publish_cmd += ["--password", password]
332
442
 
333
443
  shell_run(publish_cmd, inherit_stdio=True, cwd=self.get_path())
334
-
335
- def search_imports_in_codebase(
336
- self, searched_package: PythonPackageWorkdir
337
- ) -> list[SearchResult]:
338
- """Find import statements that reference the given package.
339
-
340
- Supports common Python forms:
341
- - from <pkg>(.<sub>)* import ...
342
- - import <pkg>(.<sub>)* [as alias]
343
-
344
- Returns a list of SearchResult with file, line and column for each match.
345
- """
346
- import re
347
-
348
- pkg = searched_package.get_package_import_name()
349
- pattern = (
350
- rf"(?m)^\s*(?:"
351
- rf"from\s+{re.escape(pkg)}(?:\.[\w\.]+)?\s+import\s+"
352
- rf"|import\s+{re.escape(pkg)}(?:\.[\w\.]+)?(?:\s+as\s+\w+)?\b"
353
- rf")"
354
- )
355
- return self.search_in_codebase(pattern, regex=True, flags=re.MULTILINE)
356
-
357
- def search_in_codebase(
358
- self, string: str, *, regex: bool = False, flags: int = 0
359
- ) -> list[SearchResult]:
360
- from wexample_filestate.utils.search_result import SearchResult
361
- from wexample_filestate_python.file.python_file import PythonFile
362
-
363
- found = []
364
-
365
- def _search(item: PythonFile) -> None:
366
- found.extend(
367
- SearchResult.create_for_all_matches(
368
- string, item, regex=regex, flags=flags
369
- )
370
- )
371
-
372
- self.for_each_child_of_type_recursive(callback=_search, class_type=PythonFile)
373
-
374
- return found
375
-
376
- def _get_readme_content(self) -> ReadmeContentConfigValue | None:
377
- from wexample_wex_addon_dev_python.config_value.python_package_readme_config_value import (
378
- PythonPackageReadmeContentConfigValue,
379
- )
380
-
381
- return PythonPackageReadmeContentConfigValue(workdir=self)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
+
4
5
  from wexample_wex_addon_app.workdir.framework_packages_suite_workdir import (
5
6
  FrameworkPackageSuiteWorkdir,
6
7
  )
@@ -11,6 +12,7 @@ if TYPE_CHECKING:
11
12
  from wexample_wex_addon_app.workdir.code_base_workdir import (
12
13
  CodeBaseWorkdir,
13
14
  )
15
+
14
16
  from wexample_wex_addon_dev_python.workdir.python_package_workdir import (
15
17
  PythonPackageWorkdir,
16
18
  )
@@ -89,7 +91,10 @@ class PythonPackagesSuiteWorkdir(FrameworkPackageSuiteWorkdir):
89
91
  return [by_name[n] for n in order]
90
92
 
91
93
  def packages_validate_internal_dependencies_declarations(self) -> None:
92
- from wexample_wex_addon_app.exception.dependency_violation_exception import DependencyViolationException
94
+ from wexample_wex_addon_app.exception.dependency_violation_exception import (
95
+ DependencyViolationException,
96
+ )
97
+
93
98
  dependencies_map = self.build_dependencies_map()
94
99
 
95
100
  self.io.log("Checking packages dependencies consistency...")
@@ -4,14 +4,30 @@ from pathlib import Path
4
4
  from typing import TYPE_CHECKING
5
5
 
6
6
  from tomlkit import TOMLDocument
7
-
7
+ from wexample_app.item.file.iml_file import ImlFile
8
+ from wexample_event.dataclass.event import Event
9
+ from wexample_event.dataclass.listener_record import EventCallback
10
+ from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
8
11
  from wexample_filestate.item.file.json_file import JsonFile
9
- from wexample_filestate_python.const.python_file import PYTHON_FILE_PYTEST_COVERAGE_JSON
12
+ from wexample_filestate.operation.abstract_operation import AbstractOperation
13
+ from wexample_filestate.operation.file_rename_operation import FileRenameOperation
14
+ from wexample_filestate_python.const.path import PATH_DIR_SRC, PATH_DIR_TESTS
15
+ from wexample_filestate_python.const.python_file import (
16
+ PYTHON_FILE_EXTENSION,
17
+ PYTHON_FILE_PYTEST_COVERAGE_JSON,
18
+ )
10
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
11
21
  from wexample_wex_addon_app.workdir.code_base_workdir import (
12
22
  CodeBaseWorkdir,
13
23
  )
14
24
 
25
+ from wexample_wex_addon_dev_python.const.python import (
26
+ PYTHON_PYTEST_COV_FORMAT_HTML,
27
+ PYTHON_PYTEST_COV_FORMAT_JSON,
28
+ PYTHON_PYTEST_COV_REPORT_DIR,
29
+ )
30
+
15
31
  if TYPE_CHECKING:
16
32
  from wexample_config.const.types import DictConfig
17
33
  from wexample_config.options_provider.abstract_options_provider import (
@@ -24,57 +40,13 @@ if TYPE_CHECKING:
24
40
  ChildrenFileFactoryOption,
25
41
  )
26
42
  from wexample_helpers.const.types import StructuredData
43
+
27
44
  from wexample_wex_addon_dev_python.file.python_package_toml_file import (
28
45
  PythonPackageTomlFile,
29
46
  )
30
47
 
31
48
 
32
49
  class PythonWorkdir(CodeBaseWorkdir):
33
- def get_venv_path(self) -> Path:
34
- return self.get_path() / ".venv"
35
-
36
- def get_venv_bin_path(self) -> Path:
37
- return self.get_venv_path() / "bin"
38
-
39
- def get_python_path(self) -> Path:
40
- return self.get_venv_bin_path() / "python"
41
-
42
- def get_python_exec_module_command(self, module_name: str) -> list[str]:
43
- return [self.get_python_path(), "-m", module_name]
44
-
45
- def test_run(self) -> None:
46
- self.shell_run_for_app(cmd=self.test_get_command())
47
-
48
- json_file = JsonFile.create_from_path(
49
- path=self.get_path() / PYTHON_FILE_PYTEST_COVERAGE_JSON
50
- )
51
- totals = json_file.read_config().search("totals", default={}).get_dict()
52
-
53
- config_file = self.get_config_file()
54
- config = config_file.read_config()
55
- config.set_by_path(
56
- "test.coverage.last_report",
57
- {
58
- "covered": totals.get("covered_lines", 0),
59
- "excluded": totals.get("excluded_lines", 0),
60
- "missing": totals.get("missing_lines", 0),
61
- "percent": totals.get("percent_covered", 0),
62
- "total": totals.get("num_statements", 0),
63
- },
64
- )
65
- config_file.write_config()
66
-
67
- def test_get_command(self) -> list[str]:
68
- cmd = self.get_python_exec_module_command("pytest")
69
- cmd.extend(
70
- [
71
- "--cov",
72
- "--cov-report=json",
73
- ]
74
- )
75
-
76
- return cmd
77
-
78
50
  def app_install(self, env: str | None = None, force: bool = False) -> bool:
79
51
  # Use standard PDM install
80
52
  return python_install_environment(path=self.get_path())
@@ -87,6 +59,9 @@ class PythonWorkdir(CodeBaseWorkdir):
87
59
  dependencies.append(Requirement(dependency).name)
88
60
  return dependencies
89
61
 
62
+ def get_main_code_file_extension(self) -> str:
63
+ return PYTHON_FILE_EXTENSION
64
+
90
65
  def get_options_providers(self) -> list[type[AbstractOptionsProvider]]:
91
66
  from wexample_filestate_python.options_provider.python_options_provider import (
92
67
  PythonOptionsProvider,
@@ -111,6 +86,61 @@ class PythonWorkdir(CodeBaseWorkdir):
111
86
 
112
87
  return string_to_kebab_case(self.get_package_import_name())
113
88
 
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
+ def get_python_exec_module_command(self, module_name: str) -> list[str]:
107
+ return [self.get_python_path(), "-m", module_name]
108
+
109
+ def get_python_path(self) -> Path:
110
+ return self.get_venv_bin_path() / "python"
111
+
112
+ def get_venv_bin_path(self) -> Path:
113
+ return self.get_venv_path() / "bin"
114
+
115
+ def get_venv_path(self) -> Path:
116
+ return self.get_path() / ".venv"
117
+
118
+ def has_coverage_changes_since_last_report(self) -> bool:
119
+ """Return True if coverage has changed since last saved report."""
120
+ last_report = (
121
+ self.app_workdir.get_config()
122
+ .search("test.coverage.last_report")
123
+ .get_dict_or_default()
124
+ )
125
+
126
+ if not last_report:
127
+ return True
128
+
129
+ current_coverage = self._run_coverage()
130
+
131
+ return current_coverage != last_report.get("percent")
132
+
133
+ def operation_add_event_listener(
134
+ self,
135
+ operation: AbstractOperation | type[AbstractOperation],
136
+ callback: EventCallback,
137
+ suffix: str | None = None,
138
+ **kwargs,
139
+ ) -> None:
140
+ self.add_event_listener(
141
+ name=operation.get_event_name(suffix=suffix), callback=callback, **kwargs
142
+ )
143
+
114
144
  def prepare_value(self, raw_value: DictConfig | None = None) -> DictConfig:
115
145
  from wexample_config.config_value.callback_render_config_value import (
116
146
  CallbackRenderConfigValue,
@@ -120,6 +150,7 @@ class PythonWorkdir(CodeBaseWorkdir):
120
150
  ChildrenFilterOption,
121
151
  )
122
152
  from wexample_helpers.helpers.array import array_dict_get_by
153
+
123
154
  from wexample_wex_addon_dev_python.file.python_package_toml_file import (
124
155
  PythonPackageTomlFile,
125
156
  )
@@ -231,6 +262,100 @@ class PythonWorkdir(CodeBaseWorkdir):
231
262
 
232
263
  return raw_value
233
264
 
265
+ def save_dependency(self, package_name: str, version: str) -> bool:
266
+ """Add or update a dependency with strict version."""
267
+ config = self.get_project_config_file()
268
+ updated = config.add_dependency(f"{package_name}=={version}")
269
+
270
+ if updated:
271
+ config.write_parsed()
272
+
273
+ return updated
274
+
275
+ def save_project_config_file(self, config: StructuredData) -> None:
276
+ """Save the project configuration to pyproject.toml."""
277
+ config_file = self.get_project_config_file()
278
+ config_file.write(config)
279
+
280
+ def test_get_command(
281
+ self, format: str = PYTHON_PYTEST_COV_FORMAT_JSON
282
+ ) -> list[str]:
283
+ cmd = self.get_python_exec_module_command("pytest")
284
+ cmd.extend(
285
+ [
286
+ "--cov",
287
+ f"--cov-report={format}",
288
+ ]
289
+ )
290
+
291
+ return cmd
292
+
293
+ def test_run(self, format: str = PYTHON_PYTEST_COV_FORMAT_JSON) -> None:
294
+ self.shell_run_for_app(cmd=self.test_get_command(format=format))
295
+
296
+ json_file = JsonFile.create_from_path(
297
+ path=self.get_path() / PYTHON_FILE_PYTEST_COVERAGE_JSON
298
+ )
299
+ totals = json_file.read_config().search("totals", default={}).get_dict()
300
+
301
+ config_file = self.get_config_file()
302
+ config = config_file.read_config()
303
+ config.set_by_path(
304
+ "test.coverage.last_report",
305
+ {
306
+ "covered": totals.get("covered_lines", 0),
307
+ "excluded": totals.get("excluded_lines", 0),
308
+ "missing": totals.get("missing_lines", 0),
309
+ "percent": totals.get("percent_covered", 0),
310
+ "total": totals.get("num_statements", 0),
311
+ },
312
+ )
313
+ config_file.write_config()
314
+
315
+ if format == PYTHON_PYTEST_COV_FORMAT_HTML:
316
+ report_path = self.get_path() / PYTHON_PYTEST_COV_REPORT_DIR / "index.html"
317
+ if report_path.exists():
318
+ self.info(f"Report: @path{{{report_path}}}")
319
+
320
+ def update_dependencies(self, dependencies_map: dict[str, str]) -> None:
321
+ """Update dependencies versions based on the provided map.
322
+
323
+ Args:
324
+ dependencies_map: Dictionary mapping package names to their new versions.
325
+ Example: {"wexample-helpers": "0.2.3", "attrs": "23.1.0"}
326
+ """
327
+ from packaging.requirements import Requirement
328
+ from packaging.utils import canonicalize_name
329
+
330
+ config_file = self.get_project_config_file()
331
+
332
+ # Canonicalize the keys in dependencies_map for consistent matching
333
+ canonical_map = {
334
+ canonicalize_name(name): version
335
+ for name, version in dependencies_map.items()
336
+ }
337
+
338
+ # Get current dependencies
339
+ current_deps = config_file.list_dependencies()
340
+
341
+ # Update each dependency if it's in the map
342
+ for dep_spec in current_deps:
343
+ try:
344
+ req = Requirement(dep_spec)
345
+ canonical_name = canonicalize_name(req.name)
346
+
347
+ # If this dependency is in our update map, update it
348
+ if canonical_name in canonical_map:
349
+ new_version = canonical_map[canonical_name]
350
+ # Use add_dependency which handles removal of old version
351
+ config_file.add_dependency(f"{req.name}=={new_version}")
352
+ except Exception:
353
+ # Skip unparsable dependencies
354
+ continue
355
+
356
+ # Save the updated config
357
+ config_file.write_parsed()
358
+
234
359
  def _create_init_children_factory(self) -> ChildrenFileFactoryOption:
235
360
  from wexample_filestate.const.disk import DiskItemType
236
361
  from wexample_filestate.const.globals import NAME_PATTERN_NO_LEADING_DOT
@@ -279,7 +404,67 @@ class PythonWorkdir(CodeBaseWorkdir):
279
404
  ChildrenFilterOption,
280
405
  )
281
406
  from wexample_filestate_python.file.python_file import PythonFile
282
- from wexample_filestate_python.option.python_option import PythonOption
407
+ from wexample_filestate_python.option.python.add_future_annotations_option import (
408
+ AddFutureAnnotationsOption,
409
+ )
410
+ from wexample_filestate_python.option.python.add_return_types_option import (
411
+ AddReturnTypesOption,
412
+ )
413
+ from wexample_filestate_python.option.python.fix_attrs_option import (
414
+ FixAttrsOption,
415
+ )
416
+ from wexample_filestate_python.option.python.fix_blank_lines_option import (
417
+ FixBlankLinesOption,
418
+ )
419
+ from wexample_filestate_python.option.python.format_option import FormatOption
420
+ from wexample_filestate_python.option.python.fstringify_option import (
421
+ FstringifyOption,
422
+ )
423
+ from wexample_filestate_python.option.python.modernize_typing_option import (
424
+ ModernizeTypingOption,
425
+ )
426
+ from wexample_filestate_python.option.python.order_class_attributes_option import (
427
+ OrderClassAttributesOption,
428
+ )
429
+ from wexample_filestate_python.option.python.order_class_docstring_option import (
430
+ OrderClassDocstringOption,
431
+ )
432
+ from wexample_filestate_python.option.python.order_class_methods_option import (
433
+ OrderClassMethodsOption,
434
+ )
435
+ from wexample_filestate_python.option.python.order_constants_option import (
436
+ OrderConstantsOption,
437
+ )
438
+ from wexample_filestate_python.option.python.order_iterable_items_option import (
439
+ OrderIterableItemsOption,
440
+ )
441
+ from wexample_filestate_python.option.python.order_main_guard_option import (
442
+ OrderMainGuardOption,
443
+ )
444
+ from wexample_filestate_python.option.python.order_module_docstring_option import (
445
+ OrderModuleDocstringOption,
446
+ )
447
+ from wexample_filestate_python.option.python.order_module_functions_option import (
448
+ OrderModuleFunctionsOption,
449
+ )
450
+ from wexample_filestate_python.option.python.order_module_metadata_option import (
451
+ OrderModuleMetadataOption,
452
+ )
453
+ from wexample_filestate_python.option.python.order_type_checking_block_option import (
454
+ OrderTypeCheckingBlockOption,
455
+ )
456
+ from wexample_filestate_python.option.python.relocate_imports_option import (
457
+ RelocateImportsOption,
458
+ )
459
+ from wexample_filestate_python.option.python.remove_unused_option import (
460
+ RemoveUnusedOption,
461
+ )
462
+ from wexample_filestate_python.option.python.sort_imports_option import (
463
+ SortImportsOption,
464
+ )
465
+ from wexample_filestate_python.option.python.unquote_annotations_option import (
466
+ UnquoteAnnotationsOption,
467
+ )
283
468
 
284
469
  return ChildrenFilterOption(
285
470
  pattern={
@@ -287,96 +472,57 @@ class PythonWorkdir(CodeBaseWorkdir):
287
472
  "type": DiskItemType.FILE,
288
473
  "python": {
289
474
  # Configured for python >= 3.12
290
- PythonOption.OPTION_NAME_ADD_FUTURE_ANNOTATIONS: True,
291
- PythonOption.OPTION_NAME_RELOCATE_IMPORTS: True,
292
- PythonOption.OPTION_NAME_REMOVE_UNUSED: True,
293
- PythonOption.OPTION_NAME_SORT_IMPORTS: True,
294
- PythonOption.OPTION_NAME_MODERNIZE_TYPING: True,
295
- PythonOption.OPTION_NAME_FSTRINGIFY: True,
296
- PythonOption.OPTION_NAME_ADD_RETURN_TYPES: True,
297
- PythonOption.OPTION_NAME_UNQUOTE_ANNOTATIONS: True,
298
- PythonOption.OPTION_NAME_FIX_ATTRS: True,
299
- PythonOption.OPTION_NAME_ORDER_TYPE_CHECKING_BLOCK: True,
300
- PythonOption.OPTION_NAME_ORDER_MODULE_DOCSTRING: True,
301
- PythonOption.OPTION_NAME_ORDER_MODULE_METADATA: True,
302
- PythonOption.OPTION_NAME_ORDER_CONSTANTS: True,
303
- PythonOption.OPTION_NAME_ORDER_ITERABLE_ITEMS: True,
304
- PythonOption.OPTION_NAME_ORDER_MODULE_FUNCTIONS: True,
305
- PythonOption.OPTION_NAME_ORDER_MAIN_GUARD: True,
306
- PythonOption.OPTION_NAME_ORDER_CLASS_DOCSTRING: True,
307
- PythonOption.OPTION_NAME_ORDER_CLASS_ATTRIBUTES: True,
308
- PythonOption.OPTION_NAME_ORDER_CLASS_METHODS: True,
309
- PythonOption.OPTION_NAME_FIX_BLANK_LINES: True,
310
- PythonOption.OPTION_NAME_FORMAT: True,
475
+ AddFutureAnnotationsOption.get_name(): True,
476
+ RelocateImportsOption.get_name(): True,
477
+ RemoveUnusedOption.get_name(): True,
478
+ SortImportsOption.get_name(): True,
479
+ ModernizeTypingOption.get_name(): True,
480
+ FstringifyOption.get_name(): True,
481
+ AddReturnTypesOption.get_name(): True,
482
+ UnquoteAnnotationsOption.get_name(): True,
483
+ FixAttrsOption.get_name(): True,
484
+ OrderTypeCheckingBlockOption.get_name(): True,
485
+ OrderModuleDocstringOption.get_name(): True,
486
+ OrderModuleMetadataOption.get_name(): True,
487
+ OrderConstantsOption.get_name(): True,
488
+ OrderIterableItemsOption.get_name(): True,
489
+ OrderModuleFunctionsOption.get_name(): True,
490
+ OrderMainGuardOption.get_name(): True,
491
+ OrderClassDocstringOption.get_name(): True,
492
+ OrderClassAttributesOption.get_name(): True,
493
+ OrderClassMethodsOption.get_name(): True,
494
+ FixBlankLinesOption.get_name(): True,
495
+ FormatOption.get_name(): True,
311
496
  },
312
497
  },
313
498
  name_pattern=r"^.*\.py$",
314
499
  recursive=True,
315
500
  )
316
501
 
317
- def get_project_config_file(self, reload: bool = True) -> PythonPackageTomlFile:
318
- from wexample_wex_addon_dev_python.file.python_package_toml_file import (
319
- PythonPackageTomlFile,
320
- )
502
+ def _get_iml_file_class(self) -> type[ImlFile]:
503
+ return PythonAppImlFile
321
504
 
322
- config_file = self.find_by_name("pyproject.toml")
323
- assert isinstance(config_file, PythonPackageTomlFile)
324
- # Read once to populate content with file source.
325
- config_file.read_text(reload=reload)
326
- return config_file
505
+ def _get_source_code_directories(self) -> [TargetFileOrDirectoryType]:
506
+ src = self.find_by_name(PATH_DIR_SRC)
327
507
 
328
- def get_project_config(self, reload: bool = True) -> TOMLDocument:
329
- """
330
- Fetch the data from the pyproject.toml file.
331
- """
332
- return self.get_project_config_file(reload=reload).read_parsed()
508
+ if src:
509
+ return [src]
333
510
 
334
- def save_dependency(self, package_name: str, version: str) -> None:
335
- """Add or update a dependency with strict version."""
336
- config = self.get_project_config_file()
337
- config.add_dependency(f"{package_name}=={version}")
338
- config.write_parsed()
511
+ return []
339
512
 
340
- def save_project_config_file(self, config: StructuredData) -> None:
341
- """Save the project configuration to pyproject.toml."""
342
- config_file = self.get_project_config_file()
343
- config_file.write(config)
513
+ def _get_test_code_directories(self) -> [TargetFileOrDirectoryType]:
514
+ tests = self.find_by_name(PATH_DIR_TESTS)
344
515
 
345
- def update_dependencies(self, dependencies_map: dict[str, str]) -> None:
346
- """Update dependencies versions based on the provided map.
516
+ if tests:
517
+ return [tests]
347
518
 
348
- Args:
349
- dependencies_map: Dictionary mapping package names to their new versions.
350
- Example: {"wexample-helpers": "0.2.3", "attrs": "23.1.0"}
351
- """
352
- from packaging.requirements import Requirement
353
- from packaging.utils import canonicalize_name
354
-
355
- config_file = self.get_project_config_file()
519
+ return []
356
520
 
357
- # Canonicalize the keys in dependencies_map for consistent matching
358
- canonical_map = {
359
- canonicalize_name(name): version
360
- for name, version in dependencies_map.items()
361
- }
362
-
363
- # Get current dependencies
364
- current_deps = config_file.list_dependencies()
365
-
366
- # Update each dependency if it's in the map
367
- for dep_spec in current_deps:
368
- try:
369
- req = Requirement(dep_spec)
370
- canonical_name = canonicalize_name(req.name)
371
-
372
- # If this dependency is in our update map, update it
373
- if canonical_name in canonical_map:
374
- new_version = canonical_map[canonical_name]
375
- # Use add_dependency which handles removal of old version
376
- config_file.add_dependency(f"{req.name}=={new_version}")
377
- except Exception:
378
- # Skip unparsable dependencies
379
- continue
521
+ def _init_listeners(self) -> None:
522
+ """Add event listeners"""
523
+ self.operation_add_event_listener(
524
+ operation=FileRenameOperation, suffix="post", callback=self._on_test_event
525
+ )
380
526
 
381
- # Save the updated config
382
- config_file.write_parsed()
527
+ def _on_test_event(self, event: Event) -> None:
528
+ 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.53
3
+ Version: 0.0.60
4
4
  Summary: Python dev addon for wex
5
5
  Author-Email: weeger <contact@wexample.com>
6
6
  License: MIT
@@ -15,9 +15,9 @@ Requires-Dist: jinja2
15
15
  Requires-Dist: networkx
16
16
  Requires-Dist: pylint
17
17
  Requires-Dist: pyright
18
- Requires-Dist: wexample-filestate-python==0.0.48
19
- Requires-Dist: wexample-wex-addon-app==0.0.49
20
- Requires-Dist: wexample-wex-core==6.0.55
18
+ Requires-Dist: wexample-filestate-python==0.0.54
19
+ Requires-Dist: wexample-wex-addon-app==0.0.51
20
+ Requires-Dist: wexample-wex-core==6.0.63
21
21
  Provides-Extra: dev
22
22
  Requires-Dist: pytest; extra == "dev"
23
23
  Requires-Dist: pytest-cov; extra == "dev"
@@ -25,7 +25,7 @@ Description-Content-Type: text/markdown
25
25
 
26
26
  # wexample-wex-addon-dev-python
27
27
 
28
- Version: 0.0.53
28
+ Version: 0.0.60
29
29
 
30
30
  Python dev addon for wex
31
31
 
@@ -44,7 +44,7 @@ First, install the required testing dependencies:
44
44
 
45
45
  Run all tests with coverage:
46
46
  ```bash
47
- .venv/bin/python -m pytest --cov
47
+ .venv/bin/python -m pytest --cov --cov-report=html
48
48
  ```
49
49
 
50
50
  ### Common Commands
@@ -1,6 +1,6 @@
1
- wexample_wex_addon_dev_python-0.0.53.dist-info/METADATA,sha256=9CcuVf5ioaUJoFnBrKESDfwlL_5XRgATtWrFYZ3gTck,6541
2
- wexample_wex_addon_dev_python-0.0.53.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
- wexample_wex_addon_dev_python-0.0.53.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
1
+ wexample_wex_addon_dev_python-0.0.60.dist-info/METADATA,sha256=Ya1RR-kM1esUeDQRedjO9O-ZqB17hSaPqnaoOO7lw5U,6559
2
+ wexample_wex_addon_dev_python-0.0.60.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
+ wexample_wex_addon_dev_python-0.0.60.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
@@ -25,9 +25,10 @@ wexample_wex_addon_dev_python/config_value/python_package_readme_config_value.py
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
+ wexample_wex_addon_dev_python/const/python.py,sha256=jxdPt5CnD0dcp4SmobEc_c7XcCkPFfX_lk3SVHsiVpM,203
28
29
  wexample_wex_addon_dev_python/file/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
30
  wexample_wex_addon_dev_python/file/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- wexample_wex_addon_dev_python/file/python_package_toml_file.py,sha256=OjN0CJP6eKyEs54dp_KZW086G0r5RmM-lQPyLhLP8oc,14936
31
+ wexample_wex_addon_dev_python/file/python_package_toml_file.py,sha256=ShJrZiiJj8-HrsD92W_1WozE02fi6Avll8VuYPQ25r0,16357
31
32
  wexample_wex_addon_dev_python/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
33
  wexample_wex_addon_dev_python/middleware/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
34
  wexample_wex_addon_dev_python/middleware/each_python_file_middleware.py,sha256=UzNEpedCYf31aNONFl0SuSJnoXRzhJhgEiTCaPark6E,2311
@@ -35,11 +36,11 @@ wexample_wex_addon_dev_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
35
36
  wexample_wex_addon_dev_python/python_addon_manager.py,sha256=Mmr9F5lOS2gbb8JTB4-vTri4Qn5Cd2Jukjk7uiTr1Hs,582
36
37
  wexample_wex_addon_dev_python/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
38
  wexample_wex_addon_dev_python/resources/readme_templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- wexample_wex_addon_dev_python/resources/readme_templates/tests.md.j2,sha256=6ellaBCay9Ggk61MgcyPok6euJMi-2cUboE2z8REzBs,1271
39
+ wexample_wex_addon_dev_python/resources/readme_templates/tests.md.j2,sha256=tKLcDwx7jhQkryXIxWr12AK-hKEaP6Rusu2MrluiABs,1289
39
40
  wexample_wex_addon_dev_python/resources/readme_templates/title.md.j2,sha256=U-q_U_WhTTwz3enrht3UTfQ9fwioaKUuJqwYyhBtCiA,64
40
41
  wexample_wex_addon_dev_python/workdir/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
42
  wexample_wex_addon_dev_python/workdir/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
- wexample_wex_addon_dev_python/workdir/python_package_workdir.py,sha256=l7-Qaei_MGvVSvXqNk4nE-3cpq49MB10wT71FOYmjT8,14317
43
- wexample_wex_addon_dev_python/workdir/python_packages_suite_workdir.py,sha256=5_wmwhJ3Ox0ougYyQJtybu6WijTTvtzjutcVvuXFY6E,6636
44
- wexample_wex_addon_dev_python/workdir/python_workdir.py,sha256=bNg7PA3danMdOdMZpv0VV4DGS1jCzxqvcB5VRf5BBmU,14547
45
- wexample_wex_addon_dev_python-0.0.53.dist-info/RECORD,,
43
+ wexample_wex_addon_dev_python/workdir/python_package_workdir.py,sha256=v-6B-rz1WBO94qC-4KzIVhkIBnabmAyV76du_Od2JGU,16398
44
+ wexample_wex_addon_dev_python/workdir/python_packages_suite_workdir.py,sha256=41bzNISqVKHirOSVkwBD9wuZokrMDrtWKUjlMRT_GyU,6664
45
+ wexample_wex_addon_dev_python/workdir/python_workdir.py,sha256=RE3neOVS8Nju9hTuTjalqe25Q4mZ4Uhx6x2y3XRv18s,19893
46
+ wexample_wex_addon_dev_python-0.0.60.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: pdm-backend (2.4.5)
2
+ Generator: pdm-backend (2.4.6)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any