wexample-wex-addon-dev-python 0.0.44__py3-none-any.whl → 0.0.45__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.
Files changed (37) hide show
  1. wexample_wex_addon_dev_python/__init__.py +0 -0
  2. wexample_wex_addon_dev_python/commands/__init__.py +0 -0
  3. wexample_wex_addon_dev_python/commands/code/__init__.py +0 -0
  4. wexample_wex_addon_dev_python/commands/code/check/__init__.py +0 -0
  5. wexample_wex_addon_dev_python/commands/code/check/mypy.py +46 -0
  6. wexample_wex_addon_dev_python/commands/code/check/pylint.py +108 -0
  7. wexample_wex_addon_dev_python/commands/code/check/pyright.py +101 -0
  8. wexample_wex_addon_dev_python/commands/code/check.py +105 -0
  9. wexample_wex_addon_dev_python/commands/code/format/__init__.py +1 -0
  10. wexample_wex_addon_dev_python/commands/code/format/black.py +43 -0
  11. wexample_wex_addon_dev_python/commands/code/format/isort.py +44 -0
  12. wexample_wex_addon_dev_python/commands/code/format.py +77 -0
  13. wexample_wex_addon_dev_python/commands/examples/__init__.py +0 -0
  14. wexample_wex_addon_dev_python/commands/examples/classes/__init__.py +0 -0
  15. wexample_wex_addon_dev_python/commands/examples/classes/example_pydantic_class_with_public_var_internaly_defined.py +42 -0
  16. wexample_wex_addon_dev_python/commands/examples/utils/__init__.py +0 -0
  17. wexample_wex_addon_dev_python/commands/examples/utils/some_example_type.py +7 -0
  18. wexample_wex_addon_dev_python/commands/examples/validate.py +20 -0
  19. wexample_wex_addon_dev_python/commands/release/__init__.py +0 -0
  20. wexample_wex_addon_dev_python/config_value/__init__.py +0 -0
  21. wexample_wex_addon_dev_python/config_value/python_package_readme_config_value.py +81 -0
  22. wexample_wex_addon_dev_python/const/__init__.py +0 -0
  23. wexample_wex_addon_dev_python/const/package.py +20 -0
  24. wexample_wex_addon_dev_python/file/__init__.py +0 -0
  25. wexample_wex_addon_dev_python/file/python_package_toml_file.py +304 -0
  26. wexample_wex_addon_dev_python/middleware/__init__.py +0 -0
  27. wexample_wex_addon_dev_python/middleware/each_python_file_middleware.py +79 -0
  28. wexample_wex_addon_dev_python/python_addon_manager.py +15 -0
  29. wexample_wex_addon_dev_python/workdir/__init__.py +0 -0
  30. wexample_wex_addon_dev_python/workdir/python_package_workdir.py +206 -0
  31. wexample_wex_addon_dev_python/workdir/python_packages_suite_workdir.py +165 -0
  32. wexample_wex_addon_dev_python/workdir/python_workdir.py +240 -0
  33. {wexample_wex_addon_dev_python-0.0.44.dist-info → wexample_wex_addon_dev_python-0.0.45.dist-info}/METADATA +8 -8
  34. wexample_wex_addon_dev_python-0.0.45.dist-info/RECORD +37 -0
  35. wexample_wex_addon_dev_python-0.0.44.dist-info/RECORD +0 -5
  36. {wexample_wex_addon_dev_python-0.0.44.dist-info → wexample_wex_addon_dev_python-0.0.45.dist-info}/WHEEL +0 -0
  37. {wexample_wex_addon_dev_python-0.0.44.dist-info → wexample_wex_addon_dev_python-0.0.45.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from wexample_filestate.config_value.readme_content_config_value import (
6
+ ReadmeContentConfigValue,
7
+ )
8
+ from wexample_wex_addon_dev_python.workdir.python_package_workdir import (
9
+ PythonPackageWorkdir,
10
+ )
11
+
12
+
13
+ class PythonPackageReadmeContentConfigValue(ReadmeContentConfigValue):
14
+ workdir: PythonPackageWorkdir
15
+
16
+ def _get_doc_path(self, section: str) -> str:
17
+ """
18
+ Returns the path to a documentation section file
19
+ """
20
+ return os.path.join(
21
+ self.workdir.get_path(), ".wex", "doc", "readme", f"{section}.md"
22
+ )
23
+
24
+ def _add_section_if_exists(self, section: str) -> str:
25
+ """
26
+ Returns section content if the documentation file exists
27
+ """
28
+ doc_path = self._get_doc_path(section)
29
+
30
+ if os.path.exists(doc_path):
31
+ with open(doc_path, encoding="utf-8") as file:
32
+ content = file.read()
33
+ return f"## {section.title()}\n\n{content}\n\n"
34
+
35
+ return ""
36
+
37
+ def get_templates(self) -> list[str] | None:
38
+ # Use TOMLDocument from the workdir
39
+ doc = self.workdir.get_project_config()
40
+ project = doc.get("project", {}) if isinstance(doc, dict) else {}
41
+
42
+ # Extract information
43
+ description = project.get("description", "")
44
+ python_version = project.get("requires-python", "")
45
+ dependencies = project.get("dependencies", [])
46
+ urls = (
47
+ project.get("urls", {}) if isinstance(project.get("urls", {}), dict) else {}
48
+ )
49
+ # Accept both lowercase and capitalized homepage key variants
50
+ homepage = urls.get("homepage") or urls.get("Homepage") or ""
51
+ license_field = project.get("license", {})
52
+ if isinstance(license_field, dict):
53
+ license_info = license_field.get("text", "") or license_field.get(
54
+ "file", ""
55
+ )
56
+ else:
57
+ license_info = str(license_field) if license_field else ""
58
+
59
+ # Format dependencies list
60
+ deps_list = "\n".join([f"- {dep}" for dep in dependencies])
61
+
62
+ package_name = self.workdir.get_package_name()
63
+ return [
64
+ f"# {package_name}\n\n"
65
+ f"{description}\n\n"
66
+ f"Version: {self.workdir.get_project_version()}\n\n"
67
+ f'{self._add_section_if_exists("features")}'
68
+ "## Requirements\n\n"
69
+ f"- Python {python_version}\n\n"
70
+ "## Dependencies\n\n"
71
+ f"{deps_list}\n\n"
72
+ "## Installation\n\n"
73
+ "```bash\n"
74
+ f"pip install {package_name}\n"
75
+ "```\n\n"
76
+ f'{self._add_section_if_exists("usage")}'
77
+ "## Links\n\n"
78
+ f"- Homepage: {homepage}\n\n"
79
+ "## License\n\n"
80
+ f"{license_info}"
81
+ ]
File without changes
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ # Names of dev/build tools to remove from runtime [project.dependencies]
4
+ # Keep this list in sync with tooling expectations.
5
+ RUNTIME_DEPENDENCY_REMOVE_NAMES: set[str] = {
6
+ "pytest",
7
+ "pip-tools",
8
+ "black",
9
+ "ruff",
10
+ "flake8",
11
+ "mypy",
12
+ "isort",
13
+ "coverage",
14
+ "build",
15
+ "twine",
16
+ "pip",
17
+ "setuptools",
18
+ "wheel",
19
+ "typing-extensions",
20
+ }
File without changes
@@ -0,0 +1,304 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from wexample_filestate.item.file.toml_file import TomlFile
6
+ from wexample_wex_core.workdir.mixin.as_suite_package_item import (
7
+ AsSuitePackageItem,
8
+ )
9
+
10
+ if TYPE_CHECKING:
11
+ from tomlkit import TOMLDocument
12
+ from wexample_wex_core.workdir.code_base_workdir import (
13
+ CodeBaseWorkdir,
14
+ )
15
+
16
+
17
+ class PythonPackageTomlFile(AsSuitePackageItem, TomlFile):
18
+
19
+ def _project_table(self):
20
+ """Ensure and return the [project] table."""
21
+ from wexample_filestate_python.helpers.toml import toml_ensure_table
22
+
23
+ doc = self.read_parsed()
24
+ project, _ = toml_ensure_table(doc, ["project"])
25
+ return project
26
+
27
+ def _dependencies_array(self):
28
+ """Ensure and return project.dependencies as a multi-line TOML array."""
29
+ from wexample_filestate_python.helpers.toml import (
30
+ toml_ensure_array,
31
+ )
32
+
33
+ project = self._project_table()
34
+ deps, _ = toml_ensure_array(project, "dependencies")
35
+ deps.multiline(True)
36
+ return deps
37
+
38
+ def _optional_group_array(self, group: str):
39
+ """Ensure and return project.optional-dependencies[group] as multi-line array."""
40
+ from wexample_filestate_python.helpers.toml import (
41
+ toml_ensure_array,
42
+ toml_ensure_table,
43
+ )
44
+
45
+ project = self._project_table()
46
+ opt, _ = toml_ensure_table(project, ["optional-dependencies"])
47
+ arr, _ = toml_ensure_array(opt, group)
48
+ arr.multiline(True)
49
+ return arr
50
+
51
+ # --- Unified dependency accessors (runtime vs optional) ---
52
+ def _get_deps_array(self, optional: bool = False, group: str = "dev"):
53
+ """Return TOML array for runtime deps or optional group (multiline)."""
54
+ return (
55
+ self._optional_group_array(group)
56
+ if optional
57
+ else self._dependencies_array()
58
+ )
59
+
60
+ def list_dependencies(
61
+ self, optional: bool = False, group: str = "dev"
62
+ ) -> list[str]:
63
+ deps = self._get_deps_array(optional=optional, group=group)
64
+ return [str(x) for x in list(deps)]
65
+
66
+ def list_dependency_names(
67
+ self,
68
+ canonicalize_names: bool = True,
69
+ optional: bool = False,
70
+ group: str = "dev",
71
+ ) -> list[str]:
72
+ """Return dependency package names derived from list_dependencies().
73
+
74
+ If canonicalize_names is True, names are normalized using packaging's
75
+ canonicalize_name for robust comparisons (dash/underscore, case, etc.).
76
+ """
77
+ from packaging.requirements import Requirement
78
+ from packaging.utils import canonicalize_name
79
+
80
+ names: list[str] = []
81
+ for spec in self.list_dependencies(optional=optional, group=group):
82
+ try:
83
+ name = Requirement(spec).name
84
+ names.append(canonicalize_name(name) if canonicalize_names else name)
85
+ except Exception:
86
+ # Skip unparsable entries when deriving names
87
+ continue
88
+ return names
89
+
90
+ def add_dependency(
91
+ self, spec: str, optional: bool = False, group: str = "dev"
92
+ ) -> bool:
93
+ from packaging.requirements import Requirement
94
+ from packaging.utils import canonicalize_name
95
+ from wexample_filestate_python.helpers.toml import toml_sort_string_array
96
+
97
+ deps = self._get_deps_array(optional=optional, group=group)
98
+ # Remove existing entries for the same package name before adding the new spec.
99
+ new_name = canonicalize_name(Requirement(spec).name)
100
+ removed = self.remove_dependency_by_name(
101
+ new_name, optional=optional, group=group
102
+ )
103
+
104
+ # Append (or re-append) the new spec if it is not already present verbatim
105
+ if spec not in deps:
106
+ deps.append(spec)
107
+ toml_sort_string_array(deps)
108
+ return True
109
+
110
+ return removed
111
+
112
+ def remove_dependency_by_name(
113
+ self, package_name: str, optional: bool = False, group: str = "dev"
114
+ ) -> bool:
115
+ """Remove all dependency entries that match the given package name.
116
+
117
+ The provided package_name can be raw; it will be canonicalized to ensure
118
+ consistent matching against entries parsed from list_dependencies().
119
+ """
120
+ from packaging.requirements import Requirement
121
+ from packaging.utils import canonicalize_name
122
+
123
+ deps = self._get_deps_array(optional=optional, group=group)
124
+
125
+ target = canonicalize_name(package_name)
126
+ filtered: list[str] = []
127
+ for existing in list(deps):
128
+ try:
129
+ existing_name = canonicalize_name(Requirement(str(existing)).name)
130
+ except Exception:
131
+ # Keep unparsable entries untouched
132
+ filtered.append(existing)
133
+ continue
134
+ if existing_name != target:
135
+ filtered.append(existing)
136
+
137
+ if len(filtered) != len(deps):
138
+ deps.clear()
139
+ deps.extend(filtered)
140
+ return True
141
+ return False
142
+
143
+ def find_package_workdir(self) -> CodeBaseWorkdir | None:
144
+ from wexample_wex_core.workdir.code_base_workdir import (
145
+ CodeBaseWorkdir,
146
+ )
147
+
148
+ return self.find_closest(CodeBaseWorkdir)
149
+
150
+ def dumps(self, content: TOMLDocument | dict | None = None) -> str:
151
+ """Serialize a TOMLDocument (preferred) or a plain dict to TOML.
152
+ Using tomlkit.dumps preserves comments/formatting when content is a TOMLDocument.
153
+ """
154
+ from tomlkit import dumps, table
155
+ from wexample_filestate_python.helpers.package import package_normalize_name
156
+ from wexample_filestate_python.helpers.toml import (
157
+ toml_ensure_array_multiline,
158
+ toml_ensure_table,
159
+ toml_get_string_value,
160
+ toml_set_array_multiline,
161
+ toml_sort_string_array,
162
+ )
163
+ from wexample_wex_addon_dev_python.const.package import (
164
+ RUNTIME_DEPENDENCY_REMOVE_NAMES,
165
+ )
166
+
167
+ # Obtain the current TOML document (preserving formatting) if not provided
168
+ content = content or self.read_parsed()
169
+
170
+ # Try to get current package/workdir context
171
+ package = self.find_package_workdir()
172
+ import_name: str | None = None
173
+ project_version: str | None = None
174
+ project_name: str | None = None
175
+ if package:
176
+ project_name = package.get_package_name()
177
+ project_version = package.get_project_version()
178
+ import_name = package.get_package_import_name()
179
+
180
+ # --- [build-system] enforcement ---
181
+ build_tbl = content.get("build-system") if isinstance(content, dict) else None
182
+ if not build_tbl or not isinstance(build_tbl, dict):
183
+ build_tbl = table()
184
+ content["build-system"] = build_tbl
185
+ desired_requires = ["pdm-backend"]
186
+ if build_tbl.get("requires") != desired_requires:
187
+ build_tbl["requires"] = desired_requires
188
+ if build_tbl.get("build-backend") != "pdm.backend":
189
+ build_tbl["build-backend"] = "pdm.backend"
190
+
191
+ # --- [tool.pdm.build] enforcement ---
192
+ tool_tbl, _ = toml_ensure_table(content, ["tool"])
193
+ pdm_tbl, _ = toml_ensure_table(tool_tbl, ["pdm"])
194
+ build_pdm_tbl, _ = toml_ensure_table(pdm_tbl, ["build"])
195
+ includes_arr, _ = toml_ensure_array_multiline(build_pdm_tbl, "includes")
196
+
197
+ pdm_tbl["distribution"] = True
198
+ # Enforce src layout, packages, and includes (py.typed)
199
+ if build_pdm_tbl.get("package-dir") != "src":
200
+ build_pdm_tbl["package-dir"] = "src"
201
+ if import_name:
202
+ desired_pkgs = [{"include": import_name, "from": "src"}]
203
+ if build_pdm_tbl.get("packages") != desired_pkgs:
204
+ build_pdm_tbl["packages"] = desired_pkgs
205
+ desired_includes = [f"src/{import_name}/*"]
206
+ current_includes = [str(x) for x in list(includes_arr)]
207
+ if current_includes != desired_includes:
208
+ toml_set_array_multiline(build_pdm_tbl, "includes", desired_includes)
209
+
210
+ # --- [project] table and basic fields ---
211
+ project_tbl, _ = toml_ensure_table(content, ["project"])
212
+ # Name sync (best-effort)
213
+ if project_name:
214
+ project_tbl["name"] = project_name
215
+ # Version sync (best-effort)
216
+ if project_version:
217
+ project_tbl["version"] = project_version
218
+ # Python requirement
219
+ target_requires_python = ">=3.10"
220
+ if project_tbl.get("requires-python") != target_requires_python:
221
+ project_tbl["requires-python"] = target_requires_python
222
+
223
+ # --- Dependencies normalization ---
224
+ # Use class helper to ensure multiline dependencies array
225
+ deps_arr = self._dependencies_array()
226
+ # Sort dependencies array
227
+ toml_sort_string_array(deps_arr)
228
+
229
+ # Optional dependency groups
230
+ opt_tbl, _ = toml_ensure_table(project_tbl, ["optional-dependencies"])
231
+ # Ensure dev group exists (multiline)
232
+ dev_arr = self._optional_group_array("dev")
233
+
234
+ # Filestate configuration for keep/exclude-add
235
+ filestate_tbl = None
236
+ if isinstance(tool_tbl, dict):
237
+ filestate_tbl = tool_tbl.get("filestate")
238
+ keep_names: set[str] = set()
239
+ exclude_add: set[str] = set()
240
+ if isinstance(filestate_tbl, dict):
241
+ keep_list = filestate_tbl.get("keep")
242
+ if isinstance(keep_list, list):
243
+ keep_names = {package_normalize_name(str(x)) for x in keep_list}
244
+ ex_list = filestate_tbl.get("exclude-add")
245
+ if isinstance(ex_list, list):
246
+ exclude_add = {str(x).strip().lower() for x in ex_list}
247
+
248
+ # Remove unwanted dev/build tools from runtime deps (unless kept)
249
+
250
+ def _should_remove(item: object) -> bool:
251
+ name = package_normalize_name(toml_get_string_value(item))
252
+ if name in keep_names:
253
+ return False
254
+ if name == "typing-extensions":
255
+ # Safe to drop when python >= 3.10 and we manage deps
256
+ return True
257
+ return name in RUNTIME_DEPENDENCY_REMOVE_NAMES
258
+
259
+ to_keep = []
260
+ for it in list(deps_arr):
261
+ if not _should_remove(it):
262
+ to_keep.append(it)
263
+ if len(to_keep) != len(deps_arr):
264
+ deps_arr.clear()
265
+ deps_arr.extend(to_keep)
266
+ toml_sort_string_array(deps_arr)
267
+
268
+ # Normalize any pydantic spec to pydantic>=2,<3
269
+ normalized = False
270
+ new_deps = []
271
+ for it in list(deps_arr):
272
+ val = toml_get_string_value(it).strip()
273
+ base = package_normalize_name(val)
274
+ if base == "pydantic":
275
+ new_deps.append("pydantic>=2,<3")
276
+ normalized = True
277
+ else:
278
+ new_deps.append(it)
279
+ if normalized:
280
+ deps_arr.clear()
281
+ deps_arr.extend(new_deps)
282
+ toml_sort_string_array(deps_arr)
283
+
284
+ # Ensure pydantic>=2,<3 present unless excluded
285
+ existing_norm = {
286
+ package_normalize_name(toml_get_string_value(it)) for it in list(deps_arr)
287
+ }
288
+ if "pydantic" not in exclude_add and "pydantic" not in existing_norm:
289
+ deps_arr.append("pydantic>=2,<3")
290
+ toml_sort_string_array(deps_arr)
291
+
292
+ # Ensure optional dev group contains pytest unless already in runtime deps
293
+ runtime_has_pytest = any(
294
+ package_normalize_name(toml_get_string_value(it)) == "pytest"
295
+ for it in list(deps_arr)
296
+ )
297
+ dev_values = [toml_get_string_value(it) for it in list(dev_arr)]
298
+ if not runtime_has_pytest and not any(
299
+ v.strip() == "pytest" for v in dev_values
300
+ ):
301
+ dev_arr.append("pytest")
302
+ toml_sort_string_array(dev_arr)
303
+
304
+ return dumps(content)
File without changes
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import os.path
4
+ from typing import TYPE_CHECKING
5
+
6
+ from wexample_wex_core.middleware.each_file_middleware import EachFileMiddleware
7
+
8
+ if TYPE_CHECKING:
9
+ from wexample_wex_core.common.command_request import CommandRequest
10
+
11
+
12
+ class EachPythonFileMiddleware(EachFileMiddleware):
13
+ """
14
+ Middleware for processing Python files only.
15
+ - Filters files by .py extension by default
16
+ - Ignores special directories like __pycache__ during recursion
17
+ """
18
+
19
+ # Default extension to filter
20
+ python_extension_only: bool = True
21
+
22
+ # Default list of directories to ignore during recursion
23
+ ignored_directories: set[str] = {
24
+ "__pycache__",
25
+ ".git",
26
+ ".idea",
27
+ ".vscode",
28
+ "venv",
29
+ "env",
30
+ "node_modules",
31
+ ".pytest_cache",
32
+ ".mypy_cache",
33
+ ".ruff_cache",
34
+ }
35
+
36
+ def __init__(self, **kwargs) -> None:
37
+ # Allow overriding the default settings
38
+ if "python_extension_only" in kwargs:
39
+ self.python_extension_only = kwargs.pop("python_extension_only")
40
+
41
+ if "ignored_directories" in kwargs:
42
+ self.ignored_directories = set(kwargs.pop("ignored_directories"))
43
+
44
+ super().__init__(**kwargs)
45
+
46
+ def _should_process_item(self, request: CommandRequest, item_path: str) -> bool:
47
+ """
48
+ Only process Python files based on extension.
49
+
50
+ Args:
51
+ item_path: Path to the item to check
52
+
53
+ Returns:
54
+ True if the item should be processed, False otherwise
55
+ """
56
+ # First check if it's a file (parent class behavior)
57
+ if not os.path.isfile(item_path):
58
+ return False
59
+
60
+ # If python_extension_only is enabled, check file extension
61
+ if self.python_extension_only:
62
+ return item_path.endswith(".py")
63
+
64
+ # Otherwise, accept all files
65
+ return True
66
+
67
+ def _should_explore_directory(
68
+ self, request: CommandRequest, directory_name: str
69
+ ) -> bool:
70
+ """
71
+ Skip directories that are in the ignored_directories list.
72
+
73
+ Args:
74
+ directory_name: Name of the directory to check
75
+
76
+ Returns:
77
+ False if the directory is in the ignored list, True otherwise
78
+ """
79
+ return directory_name not in self.ignored_directories
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from wexample_wex_core.common.abstract_addon_manager import AbstractAddonManager
4
+ from wexample_wex_core.middleware.abstract_middleware import AbstractMiddleware
5
+
6
+
7
+ class PythonAddonManager(AbstractAddonManager):
8
+ def get_middlewares_classes(self) -> list[type[AbstractMiddleware]]:
9
+ from wexample_wex_addon_dev_python.middleware.each_python_file_middleware import (
10
+ EachPythonFileMiddleware,
11
+ )
12
+
13
+ return [
14
+ EachPythonFileMiddleware,
15
+ ]
File without changes