wexample-wex-addon-dev-python 13.3.0__py3-none-any.whl → 15.0.0__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 (18) hide show
  1. wexample_wex_addon_dev_python/python_addon_manager.py +9 -7
  2. wexample_wex_addon_dev_python/resources/ai/agents/describer/agent.yml +1 -0
  3. wexample_wex_addon_dev_python/resources/ai/agents/describer/context.j2 +9 -6
  4. wexample_wex_addon_dev_python/selection/abstract_python_code_selection.py +2 -4
  5. wexample_wex_addon_dev_python/selection/package_description_missing_selection.py +40 -0
  6. wexample_wex_addon_dev_python/selection/python_code_performance_selection.py +2 -4
  7. wexample_wex_addon_dev_python/workdir/python_package_workdir.py +25 -1
  8. wexample_wex_addon_dev_python/workdir/python_workdir.py +0 -10
  9. {wexample_wex_addon_dev_python-13.3.0.dist-info → wexample_wex_addon_dev_python-15.0.0.dist-info}/METADATA +12 -10
  10. {wexample_wex_addon_dev_python-13.3.0.dist-info → wexample_wex_addon_dev_python-15.0.0.dist-info}/RECORD +12 -17
  11. {wexample_wex_addon_dev_python-13.3.0.dist-info → wexample_wex_addon_dev_python-15.0.0.dist-info}/WHEEL +1 -1
  12. wexample_wex_addon_dev_python/operation/__init__.py +0 -0
  13. wexample_wex_addon_dev_python/operation/generated_description_operation.py +0 -127
  14. wexample_wex_addon_dev_python/option/__init__.py +0 -0
  15. wexample_wex_addon_dev_python/option/generated_description_option.py +0 -77
  16. wexample_wex_addon_dev_python/options_provider/__init__.py +0 -0
  17. wexample_wex_addon_dev_python/options_provider/generative_options_provider.py +0 -26
  18. {wexample_wex_addon_dev_python-13.3.0.dist-info → wexample_wex_addon_dev_python-15.0.0.dist-info}/entry_points.txt +0 -0
@@ -2,23 +2,22 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ from wexample_wex_addon_ai.agent.agent_contributing_addon_mixin import (
6
+ AgentContributingAddonMixin,
7
+ )
5
8
  from wexample_wex_addon_ai.formatter.formatter_contributing_addon_mixin import (
6
9
  FormatterContributingAddonMixin,
7
10
  )
8
- from wexample_wex_addon_ai.selection.selection_contributing_addon_mixin import (
9
- SelectionContributingAddonMixin,
10
- )
11
11
  from wexample_wex_core.common.abstract_addon_manager import AbstractAddonManager
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from wexample_cli.middleware.abstract_middleware import AbstractMiddleware
15
15
  from wexample_wex_addon_ai.formatter.abstract_formatter import AbstractFormatter
16
- from wexample_wex_addon_ai.selection.abstract_selection import AbstractSelection
17
16
 
18
17
 
19
18
  class PythonAddonManager(
19
+ AgentContributingAddonMixin,
20
20
  FormatterContributingAddonMixin,
21
- SelectionContributingAddonMixin,
22
21
  AbstractAddonManager,
23
22
  ):
24
23
  def get_formatter_classes(self) -> list[type[AbstractFormatter]]:
@@ -52,12 +51,15 @@ class PythonAddonManager(
52
51
  EachPythonFileMiddleware,
53
52
  ]
54
53
 
55
- def get_selection_classes(self) -> list[type[AbstractSelection]]:
54
+ def get_process_selection_classes(self) -> list[type]:
55
+ from wexample_wex_addon_dev_python.selection.package_description_missing_selection import (
56
+ PackageDescriptionMissingSelection,
57
+ )
56
58
  from wexample_wex_addon_dev_python.selection.python_code_performance_selection import (
57
59
  PythonCodePerformanceSelection,
58
60
  )
59
61
 
60
- return [PythonCodePerformanceSelection]
62
+ return [PackageDescriptionMissingSelection, PythonCodePerformanceSelection]
61
63
 
62
64
  def get_workdir_types(self) -> dict[str, type]:
63
65
  from wexample_wex_addon_dev_python.workdir.python_package_workdir import (
@@ -6,3 +6,4 @@ allowed_tools:
6
6
  - Read
7
7
  - Glob
8
8
  - Grep
9
+ - Edit
@@ -1,7 +1,7 @@
1
1
  ## Domain: describing one Python package in one sentence
2
2
 
3
- You are given a package directory. You read it and answer with a single sentence saying
4
- what the package is for. That sentence lands in `global.description`, from which it is
3
+ You are given a package directory. You read it and write a single sentence saying what
4
+ the package is for. That sentence lands in `global.description`, from which it is
5
5
  copied verbatim into `pyproject.toml` and shown on the package registry.
6
6
 
7
7
  ### Read before you write
@@ -28,9 +28,12 @@ file tree and rectifies the disk to match it" beats "provides file management ut
28
28
 
29
29
  ### Answering
30
30
 
31
- The sentence goes into `report_value`, and only what you pass there is written to the
32
- config file. Text you type outside that call is read by nobody.
31
+ The target is the package's `.wex/config.yml`. When you are asked to do the work, set
32
+ `global.description` there to the sentence — that one field, nothing else in the file
33
+ touched — then report it as a change (`described the package`). When you are asked to
34
+ change nothing, report the missing description as a finding (`description_missing`) and
35
+ the sentence you would write as a change.
33
36
 
34
37
  If the package is too empty to describe honestly — no source, no README, nothing but
35
- scaffolding — end your turn without calling `report_value`. The host writes nothing and
36
- leaves the field for a human. Guessing is the one failure that matters here.
38
+ scaffolding — write nothing and report it as a finding, code `nothing_to_describe`. The
39
+ field stays for a human. Guessing is the one failure that matters here.
@@ -2,13 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  from typing import ClassVar
4
4
 
5
- from wexample_helpers.decorator.base_class import base_class
6
- from wexample_wex_addon_ai.selection.abstract_code_from_directory_selection import (
5
+ from wexample_wex_addon_process.selection.abstract_code_from_directory_selection import (
7
6
  AbstractCodeFromDirectorySelection,
8
7
  )
9
8
 
10
9
 
11
- @base_class
12
10
  class AbstractPythonCodeSelection(AbstractCodeFromDirectorySelection):
13
11
  """Shared base for every Python-source Selection (perf, tests, …).
14
12
 
@@ -23,7 +21,7 @@ class AbstractPythonCodeSelection(AbstractCodeFromDirectorySelection):
23
21
  - 50-byte floor: drops empty stubs (chosen empirically in the shell sweep,
24
22
  kept verbatim until we have a reason to change it).
25
23
 
26
- Concrete subclasses just pick a `SELECTION_NAME` and, if needed, extend
24
+ Concrete subclasses just pick a `name` and, if needed, extend
27
25
  `get_excluded_path_fragments()` / `get_excluded_file_names()` with their
28
26
  use-case specifics via `super() + (…)`.
29
27
  """
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, ClassVar
4
+
5
+ from wexample_wex_addon_process.selection.abstract_process_selection import (
6
+ AbstractProcessSelection,
7
+ )
8
+
9
+ if TYPE_CHECKING:
10
+ from pathlib import Path
11
+
12
+
13
+ class PackageDescriptionMissingSelection(AbstractProcessSelection):
14
+ """The package's `.wex/config.yml`, when its `global.description` is empty.
15
+
16
+ The description is copied verbatim into `pyproject.toml` and shown on the
17
+ package registry. Selecting the config file itself makes the item what is to be
18
+ edited: the agent writes the one field, and nothing else in the file.
19
+ """
20
+
21
+ label: ClassVar[str] = "Python package without a description"
22
+ name: ClassVar[str] = "package-description-missing"
23
+
24
+ def instructions(self, workdir: Path, item: Path) -> str:
25
+ return (
26
+ f"Package directory: {workdir}\n"
27
+ f"Write the description as `global.description` in this file, and change "
28
+ f"nothing else in it."
29
+ )
30
+
31
+ def items(self, workdir: Path) -> list[Path]:
32
+ import yaml
33
+ from wexample_app.const.globals import WORKDIR_SETUP_DIR
34
+
35
+ config = workdir / WORKDIR_SETUP_DIR / "config.yml"
36
+ if not config.is_file():
37
+ return []
38
+ values = yaml.safe_load(config.read_text(encoding="utf-8")) or {}
39
+ description = (values.get("global") or {}).get("description")
40
+ return [] if description else [config]
@@ -3,8 +3,6 @@ from __future__ import annotations
3
3
  import ast
4
4
  from typing import TYPE_CHECKING, ClassVar
5
5
 
6
- from wexample_helpers.decorator.base_class import base_class
7
-
8
6
  from wexample_wex_addon_dev_python.selection.abstract_python_code_selection import (
9
7
  AbstractPythonCodeSelection,
10
8
  )
@@ -17,7 +15,6 @@ if TYPE_CHECKING:
17
15
  _FUNC_TYPES: tuple[type, ...] = (ast.FunctionDef, ast.AsyncFunctionDef)
18
16
 
19
17
 
20
- @base_class
21
18
  class PythonCodePerformanceSelection(AbstractPythonCodeSelection):
22
19
  """Python source files candidate for the performance sweep.
23
20
 
@@ -27,7 +24,8 @@ class PythonCodePerformanceSelection(AbstractPythonCodeSelection):
27
24
  `vendor/`), extend the inherited methods via `super() + (…)`.
28
25
  """
29
26
 
30
- SELECTION_NAME: ClassVar[str] = "python-performance"
27
+ label: ClassVar[str] = "Python code worth optimizing"
28
+ name: ClassVar[str] = "python-performance"
31
29
 
32
30
  def get_required_path_fragments(self) -> tuple[str, ...]:
33
31
  """Only library code under `/src/` — the modern Python layout for any
@@ -27,6 +27,16 @@ if TYPE_CHECKING:
27
27
  class PythonPackageWorkdir(PythonWorkdir):
28
28
  _project_info_cache = None
29
29
 
30
+ @staticmethod
31
+ def path_belongs_to_tests(package_path: Path, file_path: Path) -> bool:
32
+ """Whether a file lives under the package's own `tests/` directory.
33
+
34
+ Only that one directory, at the package root. A `testing/` package under `src/`
35
+ is shipped code — other packages import it to build their own suites — and has
36
+ every reason to be judged like the rest.
37
+ """
38
+ return (package_path / "tests") in file_path.parents
39
+
30
40
  def check_publish_prerequisites(self) -> None:
31
41
  import shutil
32
42
 
@@ -162,6 +172,13 @@ class PythonPackageWorkdir(PythonWorkdir):
162
172
  - import <pkg>(.<sub>)* [as alias]
163
173
 
164
174
  Returns a list of SearchResult with file, line and column for each match.
175
+
176
+ `tests/` is left out. What this feeds is the runtime dependency graph — what a
177
+ consumer installing the package receives — and the test suite is not part of it:
178
+ setuptools does not ship it and coverage omits it. A package a test imports
179
+ belongs in the `dev` extra, which this graph is right not to know about; judging
180
+ a test import against `project.dependencies` asks a correctly declared package
181
+ to declare it twice, in the wrong place.
165
182
  """
166
183
  import re
167
184
 
@@ -173,7 +190,14 @@ class PythonPackageWorkdir(PythonWorkdir):
173
190
  rf"|import\s+{escaped}(?:\.[\w\.]+)?(?:\s+as\s+\w+)?\b"
174
191
  rf")"
175
192
  )
176
- return self.search_in_codebase(pattern, regex=True, flags=re.MULTILINE)
193
+ results = self.search_in_codebase(pattern, regex=True, flags=re.MULTILINE)
194
+ package_path = self.get_path()
195
+
196
+ return [
197
+ result
198
+ for result in results
199
+ if not self.path_belongs_to_tests(package_path, result.item.get_path())
200
+ ]
177
201
 
178
202
  def search_in_codebase(
179
203
  self, string: str, *, regex: bool = False, flags: int = 0
@@ -118,14 +118,9 @@ class PythonWorkdir(
118
118
  PythonOptionsProvider,
119
119
  )
120
120
 
121
- from wexample_wex_addon_dev_python.options_provider.generative_options_provider import (
122
- GenerativeOptionsProvider,
123
- )
124
-
125
121
  options = super().get_options_providers()
126
122
 
127
123
  options.append(PythonOptionsProvider)
128
- options.append(GenerativeOptionsProvider)
129
124
 
130
125
  return options
131
126
 
@@ -212,16 +207,11 @@ class PythonWorkdir(
212
207
  from wexample_wex_addon_dev_python.file.python_pyproject_toml_file import (
213
208
  PythonPyprojectTomlFile,
214
209
  )
215
- from wexample_wex_addon_dev_python.option.generated_description_option import (
216
- GeneratedDescriptionOption,
217
- )
218
210
 
219
211
  raw_value = super().prepare_value(raw_value=raw_value)
220
212
 
221
213
  self.append_agents(config=raw_value)
222
214
 
223
- raw_value[GeneratedDescriptionOption.get_name()] = True
224
-
225
215
  children = raw_value["children"]
226
216
 
227
217
  self.add_gitignore_rules(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wexample-wex-addon-dev-python
3
- Version: 13.3.0
3
+ Version: 15.0.0
4
4
  Summary: Extends wex with commands to format, lint, and rename Python symbols, plus workdir types for Python packages and suites
5
5
  Author-Email: weeger <contact@wexample.com>
6
6
  License: MIT
@@ -18,9 +18,9 @@ Requires-Dist: networkx
18
18
  Requires-Dist: pylint
19
19
  Requires-Dist: pyright
20
20
  Requires-Dist: wexample-api>=6.8.0
21
- Requires-Dist: wexample-filestate-python>=9.0.0
22
- Requires-Dist: wexample-wex-addon-ai>=15.0.0
23
- Requires-Dist: wexample-wex-addon-app>=31.0.0
21
+ Requires-Dist: wexample-filestate-python>=9.1.0
22
+ Requires-Dist: wexample-wex-addon-ai>=16.0.0
23
+ Requires-Dist: wexample-wex-addon-app>=32.0.0
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: pytest; extra == "dev"
26
26
  Requires-Dist: pytest-cov; extra == "dev"
@@ -28,7 +28,7 @@ Description-Content-Type: text/markdown
28
28
 
29
29
  # wex_addon_dev_python
30
30
 
31
- Version: 13.3.0
31
+ Version: 15.0.0
32
32
 
33
33
  `wex_addon_dev_python` extends the wex CLI with a `python::` command group that formats source files with isort and black (`python::code/format`), checks them with mypy, pylint, and pyright (`python::code/check`), and renames Python symbols — packages, modules, classes, functions, constants — while propagating every affected import statement across the workdir (`python::code/rename`). It also ships `PythonPackageWorkdir` and `PythonPackagesSuiteWorkdir`, two workdir types that let wex manage a single PDM-based package or a multi-package suite as a first-class project structure. The intended users are wex operators who develop and maintain Python packages within the wexample ecosystem.
34
34
 
@@ -215,12 +215,14 @@ src/wexample_wex_addon_dev_python/selection/abstract_python_code_selection.py es
215
215
 
216
216
  src/wexample_wex_addon_dev_python/selection/python_code_performance_selection.py inherits that baseline and adds two constraints: the file must be under `/src/`, and its AST must contain at least one non-trivial function. Trivial is defined as: `pass`, `...`, docstring-only, `raise NotImplementedError`, a same-name `super()` pass-through, or any `@abstractmethod`.
217
217
 
218
+ src/wexample_wex_addon_dev_python/selection/package_description_missing_selection.py selects the package's own `.wex/config.yml`, and only when `global.description` is empty. Selecting the config file rather than the package makes the item what is to be edited, so the agent writes that one field and leaves the rest of the file alone. The description is copied verbatim into `pyproject.toml` and shown on the package registry.
219
+
220
+ This replaced a filestate operation that called the agent from inside `apply_operation`. Writing a description is a job given to an agent, not a file state to converge on: rectification retried it every pass, and an agent declining to invent a description for a package with no code kept the hole open, which kept the retry going.
221
+
218
222
  ### Supporting pieces
219
223
 
220
224
  src/wexample_wex_addon_dev_python/common/pypi_registry_gateway.py is a thin HTTP client over the PEP 503 Simple index. It accepts optional Basic auth (`username` + `token`) and exposes `has_version(package, version) -> bool`.
221
225
 
222
- src/wexample_wex_addon_dev_python/operation/generated_description_operation.py is a filestate operation that invokes a `ClaudeAgent` to produce a one-sentence `global.description` and writes it to the app config. The model is called only during `apply_operation`, not during dry-run, so a preview is always offline. Failures (network, model, length over 200 chars) leave the key unset and log a warning rather than aborting the rectification.
223
-
224
226
  src/wexample_wex_addon_dev_python/services/python/app_service.py contributes the initial `pyproject.toml` and `src/app/` scaffolding when a new Python app service is set up.
225
227
 
226
228
  src/wexample_wex_addon_dev_python/const/tags.py declares the `DomainTag` constants (`domain:format`, `domain:language-python`, `domain:lint`, `domain:service`) used in command `@command(tags=[…])` decorators.
@@ -255,9 +257,9 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
255
257
  - pylint:
256
258
  - pyright:
257
259
  - wexample-api: >=6.8.0
258
- - wexample-filestate-python: >=9.0.0
259
- - wexample-wex-addon-ai: >=15.0.0
260
- - wexample-wex-addon-app: >=31.0.0
260
+ - wexample-filestate-python: >=9.1.0
261
+ - wexample-wex-addon-ai: >=16.0.0
262
+ - wexample-wex-addon-app: >=32.0.0
261
263
 
262
264
  ## Versioning & Compatibility Policy
263
265
 
@@ -1,6 +1,6 @@
1
- wexample_wex_addon_dev_python-13.3.0.dist-info/METADATA,sha256=RKsmYndSJGicT-8AHSCtiZ2u3rSP8JqX4etsRQMR3Gc,17165
2
- wexample_wex_addon_dev_python-13.3.0.dist-info/WHEEL,sha256=VP-D4TPS230sME9Z3vb3INXvo1yt0924YRm5AOsk_dE,90
3
- wexample_wex_addon_dev_python-13.3.0.dist-info/entry_points.txt,sha256=4WCjdYUa09eJPSM1ahhnId0BTg7ncgmLgeKtfKjuUy4,127
1
+ wexample_wex_addon_dev_python-15.0.0.dist-info/METADATA,sha256=MgwnAfPzRi_h_tqrpVjGJr4I7M_tUqBR6rgPV8zYD-c,17482
2
+ wexample_wex_addon_dev_python-15.0.0.dist-info/WHEEL,sha256=Y8xFLOH-FrS4gLE6ON4ti06sl2W_9QUDiRaTK3tfwig,91
3
+ wexample_wex_addon_dev_python-15.0.0.dist-info/entry_points.txt,sha256=4WCjdYUa09eJPSM1ahhnId0BTg7ncgmLgeKtfKjuUy4,127
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
@@ -41,14 +41,8 @@ wexample_wex_addon_dev_python/helper/pdm.py,sha256=Q75RgR4xnJCQaGSY24Y4O3Y7qLSyU
41
41
  wexample_wex_addon_dev_python/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
42
  wexample_wex_addon_dev_python/middleware/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
43
  wexample_wex_addon_dev_python/middleware/each_python_file_middleware.py,sha256=0BmUS1BlL8NY_imwBtykWxf4dnabA3nTrpVHhl9qjLE,2314
44
- wexample_wex_addon_dev_python/operation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- wexample_wex_addon_dev_python/operation/generated_description_operation.py,sha256=xhx0Nad4KJ2Np65ycSszM8NCOcnsJOpQvE7WF9R0tPc,4684
46
- wexample_wex_addon_dev_python/option/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
- wexample_wex_addon_dev_python/option/generated_description_option.py,sha256=K3_Bs-WEpm34SFQY3ZbxvJYi-5SVzXhZ4VBaOPzW8rg,2750
48
- wexample_wex_addon_dev_python/options_provider/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
- wexample_wex_addon_dev_python/options_provider/generative_options_provider.py,sha256=Y8rwiZ-wO8b_rJrqCoBWJd--LsRW7wJMo887ub7qRcE,799
50
44
  wexample_wex_addon_dev_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
- wexample_wex_addon_dev_python/python_addon_manager.py,sha256=nYvMLKgwr6nZu8laB9SRSVvp8kRfwwV9EI4vGofsD2Y,2643
45
+ wexample_wex_addon_dev_python/python_addon_manager.py,sha256=0ayvkqn9uJYyK7IXqHKcWRP9w8WmRJwcJDWjef-Nh94,2725
52
46
  wexample_wex_addon_dev_python/refactor/__init__.py,sha256=CYw_GXE-1VXdKYV1092XWh-YLRci02uJ-FtzW38wUWQ,209
53
47
  wexample_wex_addon_dev_python/refactor/abstract_python_path_rename_handler.py,sha256=5ySBfqkyCww5ZOllEi8_I2Md07kL3GHxpbNIjOgq2Oc,4077
54
48
  wexample_wex_addon_dev_python/refactor/abstract_python_rename_handler.py,sha256=iTEblyjbV-CO58Aoa-0Xe5rqXZjWvuvv5FU8PVU-sb0,3353
@@ -63,8 +57,8 @@ wexample_wex_addon_dev_python/refactor/python_package_rename_handler.py,sha256=Z
63
57
  wexample_wex_addon_dev_python/refactor/rename_plan.py,sha256=RcT3IZhszV451nerU0lKSeysOafRh6vpoC6X1thCozg,3590
64
58
  wexample_wex_addon_dev_python/refactor/symbol_kind.py,sha256=FeWi_o0Dx9LPWyUlFJQfeKUYrPkbk9BQfuu73_g8yic,1496
65
59
  wexample_wex_addon_dev_python/resources/.wex.yml,sha256=JdIF6nGkFcfi76yZYGlXxeWfOK4eAUSV_gAn6i3hk2w,32
66
- wexample_wex_addon_dev_python/resources/ai/agents/describer/agent.yml,sha256=AgM2PDi8yKgIaLaqi8QStzHbIx2uYBoyyKHTfCye4vc,165
67
- wexample_wex_addon_dev_python/resources/ai/agents/describer/context.j2,sha256=JcjO0-WCLI4VtDm2tz8ly6km2OZXFKzPX40rEjqqfTY,1795
60
+ wexample_wex_addon_dev_python/resources/ai/agents/describer/agent.yml,sha256=afTOu_Kd_1sGWcI-h_WSOt-zDLN_70xTnqA3xO4o0bI,174
61
+ wexample_wex_addon_dev_python/resources/ai/agents/describer/context.j2,sha256=OTFLDQhKikePO20fRviui74Upwiyp42bMi6KPw_h1ws,2026
68
62
  wexample_wex_addon_dev_python/resources/design_rules/python_code.md,sha256=Xl6oGZnhuWIzmlcCI49p6SJmLFaQAQeGpr4hqjquWC4,974
69
63
  wexample_wex_addon_dev_python/resources/docker/Dockerfile.python-profiling,sha256=5ujzXfWNN2nv8qQzCLel6dlOwLQ1XhIGZuqplCNB_vQ,290
70
64
  wexample_wex_addon_dev_python/resources/package_publish_gitlab.yml,sha256=WVMj-DeFedQak5s5FraEnM1Rx1-ut4KgPodvAVv25ZM,304
@@ -72,8 +66,9 @@ wexample_wex_addon_dev_python/resources/readme_templates/install.md.j2,sha256=PD
72
66
  wexample_wex_addon_dev_python/resources/readme_templates/tests.md.j2,sha256=tKLcDwx7jhQkryXIxWr12AK-hKEaP6Rusu2MrluiABs,1289
73
67
  wexample_wex_addon_dev_python/resources/writing_rules/python_code.md,sha256=wjApoJK2w31kBX1OpLVqcQJ4DtoOYtpTNPi5T8uRDog,2998
74
68
  wexample_wex_addon_dev_python/selection/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
- wexample_wex_addon_dev_python/selection/abstract_python_code_selection.py,sha256=TX18Iid7XLDryAmTKXOxD5pvzhNcHcDcZViZ6vWjlA4,2270
76
- wexample_wex_addon_dev_python/selection/python_code_performance_selection.py,sha256=QyWI--Q5AUrJJIkay9QHk55OBT1-1lnktAp7RYdwvDU,7932
69
+ wexample_wex_addon_dev_python/selection/abstract_python_code_selection.py,sha256=nExPnFg6nfDhydTgjtq_rkoWMEwz6hjBOzZbbwPXLgM,2192
70
+ wexample_wex_addon_dev_python/selection/package_description_missing_selection.py,sha256=AP_QI-wA-9CmIIwAvobutJJyPTWwmPeS-j2idD7xsBw,1457
71
+ wexample_wex_addon_dev_python/selection/python_code_performance_selection.py,sha256=Boel7HjvVZUeeArd48tbXqCnyRWQfCimkOT5Kqye-aE,7906
77
72
  wexample_wex_addon_dev_python/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
73
  wexample_wex_addon_dev_python/services/python/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
74
  wexample_wex_addon_dev_python/services/python/app_service.py,sha256=OeM7f-IHm7lfS2graCBVAjjqxK_lEiQRmfl9mSEqoP0,2272
@@ -95,7 +90,7 @@ wexample_wex_addon_dev_python/workdir/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCe
95
90
  wexample_wex_addon_dev_python/workdir/__pycache__/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
91
  wexample_wex_addon_dev_python/workdir/mixin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
92
  wexample_wex_addon_dev_python/workdir/mixin/with_profiling_python_workdir_mixin.py,sha256=PA_kqwe2kF5t1nKakX5Q-6fant2R18aEnZfkjr6iJ6k,3097
98
- wexample_wex_addon_dev_python/workdir/python_package_workdir.py,sha256=zbreFyVGk_u3cvR7-RV-OtVb9ojKdboJdIcaEkA083E,18439
93
+ wexample_wex_addon_dev_python/workdir/python_package_workdir.py,sha256=VJDIuJ-WrebCMt4TGRisHfQfyaj-XXoW9AgCioEWt5k,19609
99
94
  wexample_wex_addon_dev_python/workdir/python_packages_suite_workdir.py,sha256=7Jb5mKGNV2RjFtrFhUm6jbkiTfLDmy7sA97UGHITVdw,2902
100
- wexample_wex_addon_dev_python/workdir/python_workdir.py,sha256=-LXdy0lQGfLLqfpgMY68TAqBSWud4-6lu5RXKPefsAM,28620
101
- wexample_wex_addon_dev_python-13.3.0.dist-info/RECORD,,
95
+ wexample_wex_addon_dev_python/workdir/python_workdir.py,sha256=6OQlv1EMAZL3a_o_aP8eShCWiXak-TEDrRonlhH4EFw,28220
96
+ wexample_wex_addon_dev_python-15.0.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: pdm-backend (2.4.9)
2
+ Generator: pdm-backend (2.4.10)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
File without changes
@@ -1,127 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING
4
-
5
- from wexample_filestate.enum.scopes import Scope
6
- from wexample_filestate.operation.abstract_operation import AbstractOperation
7
- from wexample_helpers.classes.mixin.has_package_resources import HasPackageResources
8
- from wexample_helpers.classes.private_field import private_field
9
- from wexample_helpers.decorator.base_class import base_class
10
-
11
- if TYPE_CHECKING:
12
- from pathlib import Path
13
-
14
- CONFIG_KEY_SECTION = "global"
15
- CONFIG_KEY_NAME = "description"
16
- DESCRIPTION_MAX_LENGTH = 200
17
- DESCRIPTION_PLACEHOLDER = "<one sentence, written by the describer agent on apply>"
18
- DESCRIPTION_UNKNOWN = "UNKNOWN"
19
-
20
-
21
- @base_class
22
- class GeneratedDescriptionOperation(HasPackageResources, AbstractOperation):
23
- """Write `global.description` into the app config, asking an agent for the sentence.
24
-
25
- The model call lives here rather than in the option that builds this operation:
26
- `FileStateDryRunResult` never calls `apply_operation`, so a preview stays offline and
27
- free, and shows the placeholder instead of a sentence it would then have to produce a
28
- second time on apply.
29
- """
30
-
31
- _previous: str | None = private_field(
32
- default=None,
33
- description="Value this operation replaced, for `undo`. None when the key was absent.",
34
- )
35
-
36
- @classmethod
37
- def get_scopes(cls) -> list[Scope]:
38
- return [Scope.CONTENT]
39
-
40
- def apply_operation(self) -> None:
41
- description = self._generate()
42
- if description is None:
43
- return
44
-
45
- config_file = self.target.get_config_file()
46
- content = config_file.read_parsed() or {}
47
- section = content.setdefault(CONFIG_KEY_SECTION, {})
48
-
49
- self._previous = section.get(CONFIG_KEY_NAME)
50
- section[CONFIG_KEY_NAME] = description
51
- config_file.write_parsed(content)
52
-
53
- self.target.success(f"Description: {description}")
54
-
55
- def undo(self) -> None:
56
- config_file = self.target.get_config_file()
57
- content = config_file.read_parsed() or {}
58
- section = content.get(CONFIG_KEY_SECTION)
59
- if not isinstance(section, dict):
60
- return
61
-
62
- if self._previous is None:
63
- section.pop(CONFIG_KEY_NAME, None)
64
- else:
65
- section[CONFIG_KEY_NAME] = self._previous
66
- config_file.write_parsed(content)
67
-
68
- def _agent_config_path(self) -> Path:
69
- return self.get_resource_path("ai", "agents", "describer", "agent.yml")
70
-
71
- def _generate(self) -> str | None:
72
- """The sentence, or None when nothing should be written.
73
-
74
- Every failure mode lands on None: the hole stays, the pass reports it and moves on.
75
- A rectification must not fail because a model was unreachable.
76
- """
77
- from wexample_wex_addon_ai.agent.claude_agent import ClaudeAgent
78
- from wexample_wex_addon_ai.file.agent_yaml_file import AgentYamlFile
79
-
80
- try:
81
- agent = ClaudeAgent(
82
- io=self.target.io,
83
- app_workdir=self.target,
84
- config_file=AgentYamlFile.create_from_path(
85
- path=self._agent_config_path()
86
- ),
87
- )
88
- sentence = agent.run_value_draft(self._prompt())
89
- except Exception as exception:
90
- self.target.warning(
91
- f"Could not generate a description: {exception}. "
92
- f"Leaving {CONFIG_KEY_SECTION}.{CONFIG_KEY_NAME} unset."
93
- )
94
- return None
95
-
96
- return self._validate(sentence)
97
-
98
- def _prompt(self) -> str:
99
- return (
100
- f"Package directory: {self.target.get_path()}\n"
101
- f"Package name: {self.target.get_package_name()}\n"
102
- f"\n"
103
- f"Read the package and report its one-sentence description."
104
- )
105
-
106
- def _validate(self, sentence: str | None) -> str | None:
107
- """Guard the one shape that would still poison a config file: a runaway sentence.
108
-
109
- `report_value` already delimits the answer, so nothing has to be extracted from the
110
- agent's prose — a missing report is simply `None`.
111
- """
112
- if sentence is None:
113
- self.target.warning(
114
- f"The describer reported nothing — "
115
- f"leaving {CONFIG_KEY_SECTION}.{CONFIG_KEY_NAME} unset."
116
- )
117
- return None
118
-
119
- if len(sentence) > DESCRIPTION_MAX_LENGTH:
120
- self.target.warning(
121
- f"The describer answered {len(sentence)} characters, over the "
122
- f"{DESCRIPTION_MAX_LENGTH} allowed — leaving "
123
- f"{CONFIG_KEY_SECTION}.{CONFIG_KEY_NAME} unset."
124
- )
125
- return None
126
-
127
- return sentence
File without changes
@@ -1,77 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Callable
4
- from typing import TYPE_CHECKING, Any, Union
5
-
6
- from wexample_config.config_option.abstract_config_option import AbstractConfigOption
7
- from wexample_filestate.enum.scopes import Scope
8
- from wexample_filestate.option.mixin.option_mixin import OptionMixin
9
- from wexample_helpers.decorator.base_class import base_class
10
-
11
- if TYPE_CHECKING:
12
- from wexample_filestate.const.types_state_items import TargetFileOrDirectoryType
13
- from wexample_filestate.operation.abstract_operation import AbstractOperation
14
-
15
-
16
- @base_class
17
- class GeneratedDescriptionOption(OptionMixin, AbstractConfigOption):
18
- """Ask an agent for `global.description` when the app config has none.
19
-
20
- Only ever fills a hole: a description already written — by a human or by an earlier
21
- run — is left alone, which is what makes the option convergent. It fires once per
22
- package, then never again. To have one rewritten, clear the key.
23
-
24
- Declared on the workdir, not on `pyproject.toml`: the toml's `description` is already
25
- derived from this key by `PythonPyprojectTomlFile._enforce_project_metadata`, so
26
- filling the config is enough for the value to reach the manifest through the ordinary
27
- deterministic path.
28
- """
29
-
30
- @classmethod
31
- def get_scopes(cls) -> list[Scope]:
32
- return [Scope.CONTENT]
33
-
34
- @staticmethod
35
- def get_raw_value_allowed_type() -> Any:
36
- return Union[bool, Callable]
37
-
38
- def applicable_on_file(self) -> bool:
39
- return False
40
-
41
- def applicable_on_missing(self) -> bool:
42
- return False
43
-
44
- def create_required_operation(
45
- self, target: TargetFileOrDirectoryType, scopes: set[Scope]
46
- ) -> AbstractOperation | None:
47
- from wexample_wex_addon_dev_python.operation.generated_description_operation import (
48
- CONFIG_KEY_NAME,
49
- CONFIG_KEY_SECTION,
50
- DESCRIPTION_PLACEHOLDER,
51
- GeneratedDescriptionOperation,
52
- )
53
-
54
- value = self.get_value()
55
- raw = value.raw if value is not None else None
56
- if callable(raw):
57
- raw = raw(target)
58
- if not bool(raw):
59
- return None
60
-
61
- existing = target.get_config().search(
62
- f"{CONFIG_KEY_SECTION}.{CONFIG_KEY_NAME}"
63
- )
64
- if not existing.is_none() and existing.get_str().strip():
65
- return None
66
-
67
- return GeneratedDescriptionOperation(
68
- option=self,
69
- target=target,
70
- description=(
71
- f"Write {CONFIG_KEY_SECTION}.{CONFIG_KEY_NAME}: "
72
- f"{DESCRIPTION_PLACEHOLDER}"
73
- ),
74
- )
75
-
76
- def get_description(self) -> str:
77
- return "Have an agent write the app description when none is set"
@@ -1,26 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING
4
-
5
- from wexample_config.options_provider.abstract_options_provider import (
6
- AbstractOptionsProvider,
7
- )
8
-
9
- if TYPE_CHECKING:
10
- from wexample_config.config_option.abstract_config_option import (
11
- AbstractConfigOption,
12
- )
13
-
14
-
15
- class GenerativeOptionsProvider(AbstractOptionsProvider):
16
- @classmethod
17
- def get_options(cls) -> list[type[AbstractConfigOption]]:
18
- if "_options_cache" not in cls.__dict__:
19
- from wexample_wex_addon_dev_python.option.generated_description_option import (
20
- GeneratedDescriptionOption,
21
- )
22
-
23
- cls._options_cache: list[type[AbstractConfigOption]] = [
24
- GeneratedDescriptionOption
25
- ]
26
- return cls._options_cache