wexample-wex-addon-dev-python 10.2.0__py3-none-any.whl → 11.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 (30) hide show
  1. wexample_wex_addon_dev_python/commands/code/check/mypy.py +4 -3
  2. wexample_wex_addon_dev_python/commands/code/check/pylint.py +25 -19
  3. wexample_wex_addon_dev_python/commands/code/check/pyright.py +18 -14
  4. wexample_wex_addon_dev_python/commands/code/check.py +4 -7
  5. wexample_wex_addon_dev_python/commands/code/format/black.py +14 -15
  6. wexample_wex_addon_dev_python/commands/code/format/isort.py +14 -15
  7. wexample_wex_addon_dev_python/commands/code/format.py +8 -5
  8. wexample_wex_addon_dev_python/commands/code/rename.py +134 -0
  9. wexample_wex_addon_dev_python/commands/examples/validate.py +1 -2
  10. wexample_wex_addon_dev_python/common/pypi_registry_gateway.py +5 -3
  11. wexample_wex_addon_dev_python/config_value/python_package_readme_config_value.py +2 -8
  12. wexample_wex_addon_dev_python/file/python_app_iml_file.py +3 -2
  13. wexample_wex_addon_dev_python/file/python_pyproject_toml_file.py +7 -7
  14. wexample_wex_addon_dev_python/helpers/pdm.py +3 -2
  15. wexample_wex_addon_dev_python/middleware/each_python_file_middleware.py +12 -14
  16. wexample_wex_addon_dev_python/refactor/__init__.py +7 -0
  17. wexample_wex_addon_dev_python/refactor/python_package_rename_handler.py +215 -0
  18. wexample_wex_addon_dev_python/refactor/symbol_kind.py +19 -0
  19. wexample_wex_addon_dev_python/selection/abstract_python_code_selection.py +22 -9
  20. wexample_wex_addon_dev_python/selection/python_code_performance_selection.py +8 -9
  21. wexample_wex_addon_dev_python/services/python/commands/service/install.py +7 -10
  22. wexample_wex_addon_dev_python/services/python/commands/service/setup.py +5 -3
  23. wexample_wex_addon_dev_python/workdir/mixin/with_profiling_python_workdir_mixin.py +15 -16
  24. wexample_wex_addon_dev_python/workdir/python_package_workdir.py +9 -7
  25. wexample_wex_addon_dev_python/workdir/python_packages_suite_workdir.py +2 -3
  26. wexample_wex_addon_dev_python/workdir/python_workdir.py +86 -20
  27. {wexample_wex_addon_dev_python-10.2.0.dist-info → wexample_wex_addon_dev_python-11.0.0.dist-info}/METADATA +10 -10
  28. {wexample_wex_addon_dev_python-10.2.0.dist-info → wexample_wex_addon_dev_python-11.0.0.dist-info}/RECORD +30 -26
  29. {wexample_wex_addon_dev_python-10.2.0.dist-info → wexample_wex_addon_dev_python-11.0.0.dist-info}/WHEEL +0 -0
  30. {wexample_wex_addon_dev_python-10.2.0.dist-info → wexample_wex_addon_dev_python-11.0.0.dist-info}/entry_points.txt +0 -0
@@ -35,12 +35,13 @@ def _code_check_mypy(kernel: Kernel, file_path: str) -> bool:
35
35
  # Build and check the file
36
36
  source = BuildSource(path=file_path, module=None, text=None)
37
37
  result = build.build(sources=[source], options=options, alt_lib_path=None)
38
- if result.errors:
38
+ errors = result.errors
39
+ if errors:
39
40
  kernel.io.log_indent_up()
40
- kernel.io.error(f"Mypy errors:")
41
+ kernel.io.error("Mypy errors:")
41
42
  kernel.io.log_indent_up()
42
43
 
43
- for error in result.errors:
44
+ for error in errors:
44
45
  kernel.io.error(message=error, symbol=False)
45
46
 
46
47
  kernel.io.log_indent_down(number=2)
@@ -5,6 +5,16 @@ from typing import TYPE_CHECKING
5
5
  if TYPE_CHECKING:
6
6
  from wexample_cli.context.execution_context import ExecutionContext
7
7
 
8
+ # Pre-computed once at import time; avoids list allocation + join on every call.
9
+ _PYLINT_DISABLE_FLAG = "--disable=" + ",".join([
10
+ "missing-module-docstring",
11
+ "import-outside-toplevel",
12
+ "no-name-in-module",
13
+ "broad-exception-caught",
14
+ "c-extension-no-member",
15
+ "line-too-long",
16
+ ])
17
+
8
18
 
9
19
  def _code_check_pylint(context: ExecutionContext, file_path: str) -> bool:
10
20
  """Check a Python file using pylint for code quality.
@@ -22,23 +32,13 @@ def _code_check_pylint(context: ExecutionContext, file_path: str) -> bool:
22
32
 
23
33
  # Use subprocess to capture pylint output
24
34
  # This avoids issues with pylint's direct printing to stdout
25
- # List of warnings to disable
26
- disabled_warnings = [
27
- "missing-module-docstring",
28
- "import-outside-toplevel",
29
- "no-name-in-module",
30
- "broad-exception-caught",
31
- "c-extension-no-member",
32
- "line-too-long",
33
- ]
34
-
35
35
  cmd = [
36
36
  sys.executable,
37
37
  "-m",
38
38
  "pylint",
39
39
  file_path,
40
40
  "--output-format=json",
41
- f"--disable={','.join(disabled_warnings)}",
41
+ _PYLINT_DISABLE_FLAG,
42
42
  ]
43
43
  process = subprocess.run(cmd, capture_output=True, text=True, check=False)
44
44
 
@@ -53,19 +53,25 @@ def _code_check_pylint(context: ExecutionContext, file_path: str) -> bool:
53
53
  # Parse the JSON output
54
54
  results = json.loads(json_output)
55
55
 
56
- # Filter messages by type
57
- errors = [msg for msg in results if msg.get("type") in ("error", "fatal")]
58
- warnings = [msg for msg in results if msg.get("type") == "warning"]
59
- conventions = [
60
- msg for msg in results if msg.get("type") in ("convention", "refactor", "info")
61
- ]
56
+ # Categorise messages in a single pass instead of three separate comprehensions.
57
+ errors: list = []
58
+ warnings: list = []
59
+ conventions: list = []
60
+ for msg in results:
61
+ t = msg.get("type")
62
+ if t in ("error", "fatal"):
63
+ errors.append(msg)
64
+ elif t == "warning":
65
+ warnings.append(msg)
66
+ elif t in ("convention", "refactor", "info"):
67
+ conventions.append(msg)
62
68
 
63
69
  # Display results if any issues found
64
70
  if errors or warnings or conventions:
65
71
  # Display errors
66
72
  if errors:
67
73
  context.io.log_indent_up()
68
- context.io.error(f"Pylint errors:")
74
+ context.io.error("Pylint errors:")
69
75
  context.io.log_indent_up()
70
76
 
71
77
  for error in errors:
@@ -80,7 +86,7 @@ def _code_check_pylint(context: ExecutionContext, file_path: str) -> bool:
80
86
  # Display warnings with detailed logging
81
87
  if warnings:
82
88
  context.io.log_indent_up()
83
- context.io.warning(f"Pylint warnings:")
89
+ context.io.warning("Pylint warnings:")
84
90
  context.io.log_indent_up()
85
91
 
86
92
  for warning in warnings:
@@ -1,5 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
4
+ import subprocess
5
+ import sys
3
6
  from typing import TYPE_CHECKING
4
7
 
5
8
  if TYPE_CHECKING:
@@ -16,10 +19,6 @@ def _code_check_pyright(kernel: Kernel, file_path: str) -> bool:
16
19
  Returns:
17
20
  bool: True if check passes, False otherwise
18
21
  """
19
- import json
20
- import subprocess
21
- import sys
22
-
23
22
  # Use subprocess to run pyright
24
23
  cmd = [sys.executable, "-m", "pyright", file_path, "--outputjson"]
25
24
  process = subprocess.run(cmd, capture_output=True, text=True, check=False)
@@ -42,20 +41,25 @@ def _code_check_pyright(kernel: Kernel, file_path: str) -> bool:
42
41
  # Parse the JSON output
43
42
  results = json.loads(json_output)
44
43
 
45
- # Extract diagnostics
46
- diagnostics = results.get("diagnostics", [])
47
-
48
- # Filter by severity
49
- errors = [diag for diag in diagnostics if diag.get("severity") == "error"]
50
- warnings = [diag for diag in diagnostics if diag.get("severity") == "warning"]
51
- info = [diag for diag in diagnostics if diag.get("severity") == "information"]
44
+ # Classify diagnostics in a single pass (avoids 3× iteration and repeated .get("severity"))
45
+ errors: list = []
46
+ warnings: list = []
47
+ info: list = []
48
+ for diag in results.get("diagnostics", []):
49
+ sev = diag.get("severity")
50
+ if sev == "error":
51
+ errors.append(diag)
52
+ elif sev == "warning":
53
+ warnings.append(diag)
54
+ elif sev == "information":
55
+ info.append(diag)
52
56
 
53
57
  # Display results if any issues found
54
58
  if errors or warnings or info:
55
59
  # Display errors
56
60
  if errors:
57
61
  kernel.io.log_indent_up()
58
- kernel.io.error(f"Pyright errors:")
62
+ kernel.io.error("Pyright errors:")
59
63
 
60
64
  for error in errors:
61
65
  line = error.get("range", {}).get("start", {}).get("line", 0) + 1
@@ -70,7 +74,7 @@ def _code_check_pyright(kernel: Kernel, file_path: str) -> bool:
70
74
  # Display warnings
71
75
  if warnings:
72
76
  kernel.io.log_indent_up()
73
- kernel.io.warning(f"Pyright warnings:")
77
+ kernel.io.warning("Pyright warnings:")
74
78
 
75
79
  for warning in warnings:
76
80
  line = warning.get("range", {}).get("start", {}).get("line", 0) + 1
@@ -85,7 +89,7 @@ def _code_check_pyright(kernel: Kernel, file_path: str) -> bool:
85
89
  # Display information
86
90
  if info:
87
91
  kernel.io.log_indent_up()
88
- kernel.io.info(f"Pyright information:")
92
+ kernel.io.info("Pyright information:")
89
93
  kernel.io.log_indent_up()
90
94
 
91
95
  for item in info:
@@ -74,16 +74,13 @@ def python__code__check(
74
74
  }
75
75
 
76
76
  # Determine which tools to run
77
- if tool and tool.lower() in tool_map:
77
+ tool_lower = tool.lower() if tool else None
78
+ if tool_lower and tool_lower in tool_map:
78
79
  # Run only the specified tool
79
- check_functions = [tool_map[tool.lower()]]
80
+ check_functions = [tool_map[tool_lower]]
80
81
  else:
81
82
  # Run all tools if no specific tool is specified or if the specified tool is invalid
82
- check_functions = [
83
- _code_check_mypy,
84
- _code_check_pylint,
85
- _code_check_pyright,
86
- ]
83
+ check_functions = list(tool_map.values())
87
84
 
88
85
  # Track overall success
89
86
  all_checks_passed = True
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import subprocess
4
+ import sys
3
5
  from typing import TYPE_CHECKING
4
6
 
5
7
  if TYPE_CHECKING:
@@ -16,11 +18,8 @@ def _code_format_black(kernel: Kernel, file_path: str) -> bool:
16
18
  Returns:
17
19
  bool: True if formatting succeeds, False otherwise
18
20
  """
19
- import subprocess
20
- import sys
21
-
22
21
  # Use subprocess to run black
23
- cmd = [sys.executable, "-m", "black", file_path]
22
+ cmd = (sys.executable, "-m", "black", file_path)
24
23
  process = subprocess.run(cmd, capture_output=True, text=True, check=False)
25
24
 
26
25
  # Check if the command was successful
@@ -30,17 +29,17 @@ def _code_format_black(kernel: Kernel, file_path: str) -> bool:
30
29
  else:
31
30
  kernel.io.success(f"Black: {file_path} already well formatted")
32
31
  return True
33
- else:
34
- kernel.io.error(f"Black failed to format {file_path}")
35
- kernel.io.log_indent_up()
36
32
 
37
- if process.stderr:
38
- kernel.io.error(f"Error: {process.stderr}", symbol=False)
39
- if process.stdout:
40
- kernel.io.error(f"Output: {process.stdout}", symbol=False)
33
+ kernel.io.error(f"Black failed to format {file_path}")
34
+ kernel.io.log_indent_up()
35
+
36
+ if process.stderr:
37
+ kernel.io.error(f"Error: {process.stderr}", symbol=False)
38
+ if process.stdout:
39
+ kernel.io.error(f"Output: {process.stdout}", symbol=False)
41
40
 
42
- # Add detailed error properties
43
- kernel.io.properties({"returncode": process.returncode, "command": cmd})
41
+ # Add detailed error properties
42
+ kernel.io.properties({"returncode": process.returncode, "command": cmd})
44
43
 
45
- kernel.io.log_indent_down()
46
- return False
44
+ kernel.io.log_indent_down()
45
+ return False
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import subprocess
4
+ import sys
3
5
  from typing import TYPE_CHECKING
4
6
 
5
7
  if TYPE_CHECKING:
@@ -16,13 +18,10 @@ def _code_format_isort(kernel: Kernel, file_path: str) -> bool:
16
18
  Returns:
17
19
  bool: True if formatting succeeds, False otherwise
18
20
  """
19
- import subprocess
20
- import sys
21
-
22
21
  # Use subprocess to run isort
23
22
  # --profile=black ensures compatibility with Black formatter
24
23
  cmd = [sys.executable, "-m", "isort", "--profile=black", file_path]
25
- process = subprocess.run(cmd, capture_output=True, text=True, check=False)
24
+ process = subprocess.run(cmd, capture_output=True, text=True)
26
25
 
27
26
  # Check if the command was successful
28
27
  if process.returncode == 0:
@@ -31,17 +30,17 @@ def _code_format_isort(kernel: Kernel, file_path: str) -> bool:
31
30
  else:
32
31
  kernel.io.success(f"isort successfully reformatted imports in {file_path}")
33
32
  return True
34
- else:
35
- kernel.io.error(f"isort failed to format imports in {file_path}")
36
- kernel.io.log_indent_up()
37
33
 
38
- if process.stderr:
39
- kernel.io.error(f"Error: {process.stderr}", symbol=False)
40
- if process.stdout:
41
- kernel.io.error(f"Output: {process.stdout}", symbol=False)
34
+ kernel.io.error(f"isort failed to format imports in {file_path}")
35
+ kernel.io.log_indent_up()
36
+
37
+ if process.stderr:
38
+ kernel.io.error(f"Error: {process.stderr}", symbol=False)
39
+ if process.stdout:
40
+ kernel.io.error(f"Output: {process.stdout}", symbol=False)
42
41
 
43
- # Add detailed error properties
44
- kernel.io.properties({"returncode": process.returncode, "command": cmd})
42
+ # Add detailed error properties
43
+ kernel.io.properties({"returncode": process.returncode, "command": cmd})
45
44
 
46
- kernel.io.log_indent_down()
47
- return False
45
+ kernel.io.log_indent_down()
46
+ return False
@@ -61,13 +61,16 @@ def python__code__format(
61
61
  "isort": _code_format_isort,
62
62
  }
63
63
 
64
+ # Normalise once to avoid repeated .lower() calls
65
+ tool_lower = tool.lower() if tool else None
66
+
64
67
  # Determine which tools to run
65
- if tool and tool.lower() in tool_map:
68
+ if tool_lower in tool_map:
66
69
  # Run only the specified tool
67
- format_functions = [tool_map[tool.lower()]]
70
+ format_functions = [tool_map[tool_lower]]
68
71
  else:
69
- # Run all tools if no specific tool is specified or if the specified tool is invalid
70
- if tool and tool.lower() not in tool_map:
72
+ # In the else branch, a truthy tool_lower is already known to be absent from tool_map
73
+ if tool_lower:
71
74
  kernel.io.warning(f"Unknown tool '{tool}', running all available tools")
72
75
 
73
76
  # Run isort first, then black (recommended order)
@@ -85,7 +88,7 @@ def python__code__format(
85
88
  format_result = format_function(kernel, file)
86
89
 
87
90
  # Update overall success status
88
- all_formats_passed = all_formats_passed and format_result
91
+ all_formats_passed &= format_result
89
92
 
90
93
  # Stop if a format fails and stop_on_failure is True
91
94
  if not format_result and stop_on_failure:
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from wexample_cli.const.tags import AudienceTag, EffectTag, ScopeTag
6
+ from wexample_cli.decorator.command import command
7
+ from wexample_cli.decorator.middleware import middleware
8
+ from wexample_cli.decorator.option import option
9
+ from wexample_wex_addon_app.middleware.app_middleware import AppMiddleware
10
+ from wexample_wex_core.const.globals import COMMAND_TYPE_ADDON
11
+
12
+ from wexample_wex_addon_dev_python.const.tags import DomainTag
13
+
14
+ if TYPE_CHECKING:
15
+ from wexample_app.response.abstract_response import AbstractResponse
16
+ from wexample_cli.context.execution_context import ExecutionContext
17
+ from wexample_wex_addon_app.workdir.managed_workdir import ManagedWorkdir
18
+
19
+
20
+ @option(
21
+ name="source",
22
+ type=str,
23
+ required=True,
24
+ description="Dotted source name to rename (e.g. 'mylib.helpers').",
25
+ )
26
+ @option(
27
+ name="target",
28
+ type=str,
29
+ required=True,
30
+ description="Dotted target name (e.g. 'mylib.helper').",
31
+ )
32
+ @option(
33
+ name="kind",
34
+ type=str,
35
+ required=False,
36
+ default="package",
37
+ description="Symbol kind to rename. Step 1 supports only 'package'.",
38
+ )
39
+ @option(
40
+ name="dry_run",
41
+ type=bool,
42
+ is_flag=True,
43
+ default=False,
44
+ description="Preview impacted files without writing. Default: changes are applied.",
45
+ )
46
+ @middleware(middleware=AppMiddleware)
47
+ @command(
48
+ type=COMMAND_TYPE_ADDON,
49
+ description=(
50
+ "Rename a Python package directory and propagate the change to every "
51
+ "import statement in the workdir. Dry-run by default."
52
+ ),
53
+ tags=[
54
+ DomainTag.LANGUAGE_PYTHON,
55
+ EffectTag.WRITE,
56
+ AudienceTag.AGENT_SAFE,
57
+ ScopeTag.LOCAL,
58
+ ScopeTag.PACKAGE,
59
+ ],
60
+ )
61
+ def python__code__rename(
62
+ context: ExecutionContext,
63
+ app_workdir: ManagedWorkdir,
64
+ source: str,
65
+ target: str,
66
+ kind: str = "package",
67
+ dry_run: bool = False,
68
+ ) -> AbstractResponse:
69
+ from wexample_app.response.failure_response import FailureResponse
70
+ from wexample_app.response.table_response import TableResponse
71
+
72
+ from wexample_wex_addon_dev_python.refactor.python_package_rename_handler import (
73
+ PythonPackageRenameHandler,
74
+ )
75
+ from wexample_wex_addon_dev_python.refactor.symbol_kind import SymbolKind
76
+
77
+ try:
78
+ kind_enum = SymbolKind(kind)
79
+ except ValueError:
80
+ return FailureResponse(
81
+ kernel=context.kernel,
82
+ message=(
83
+ f"Unknown --kind {kind!r}. Valid: "
84
+ f"{', '.join([k.value for k in SymbolKind])}"
85
+ ),
86
+ )
87
+
88
+ if kind_enum is not SymbolKind.PACKAGE:
89
+ return FailureResponse(
90
+ kernel=context.kernel,
91
+ message=(
92
+ f"--kind {kind_enum.value} not implemented yet "
93
+ "(step 1 covers 'package' only)."
94
+ ),
95
+ )
96
+
97
+ workdir_path = app_workdir.get_path()
98
+ handler = PythonPackageRenameHandler(
99
+ workdir_path=workdir_path,
100
+ source=source,
101
+ target=target,
102
+ dry_run=dry_run,
103
+ )
104
+ try:
105
+ report = handler.run()
106
+ except FileNotFoundError:
107
+ return FailureResponse(
108
+ kernel=context.kernel,
109
+ message=(
110
+ f"No Python package matching '{source}' under {workdir_path}. "
111
+ "Check the dotted name (e.g. 'mylib.helpers') and that you're "
112
+ "in the right workdir."
113
+ ),
114
+ )
115
+ except ValueError as e:
116
+ # Raised on ambiguous match (multiple packages) or invalid dotted name.
117
+ return FailureResponse(kernel=context.kernel, message=str(e))
118
+
119
+ mode = "APPLIED" if not dry_run else "DRY-RUN"
120
+ context.io.log(
121
+ f"[{mode}] {report.package_dir_old} → {report.package_dir_new}"
122
+ )
123
+ context.io.log(
124
+ f"[{mode}] scanned={report.files_scanned} changed={len(report.files_changed)}"
125
+ )
126
+
127
+ rows = [
128
+ [str(p.relative_to(workdir_path))] for p in report.files_changed
129
+ ]
130
+ return TableResponse(
131
+ kernel=context.kernel,
132
+ content=rows,
133
+ headers=["File"],
134
+ )
@@ -27,5 +27,4 @@ def python__examples__validate(
27
27
  ExamplePydanticClassWithPublicVarInternallyDefined,
28
28
  )
29
29
 
30
- example_class = ExamplePydanticClassWithPublicVarInternallyDefined()
31
- context.kernel.log(example_class)
30
+ context.kernel.log(ExamplePydanticClassWithPublicVarInternallyDefined())
@@ -1,9 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
4
+
3
5
  from wexample_api.common.abstract_gateway import AbstractGateway
4
6
  from wexample_helpers.classes.field import public_field
5
7
  from wexample_helpers.decorator.base_class import base_class
6
8
 
9
+ _HAS_VERSION_STATUS_CODES: tuple[int, ...] = (200, 404)
10
+
7
11
 
8
12
  @base_class
9
13
  class PypiRegistryGateway(AbstractGateway):
@@ -27,8 +31,6 @@ class PypiRegistryGateway(AbstractGateway):
27
31
  def __attrs_post_init__(self) -> None:
28
32
  super().__attrs_post_init__()
29
33
  if self.token:
30
- import base64
31
-
32
34
  credentials = base64.b64encode(
33
35
  f"{self.username or '__token__'}:{self.token}".encode()
34
36
  ).decode()
@@ -43,7 +45,7 @@ class PypiRegistryGateway(AbstractGateway):
43
45
  """
44
46
  response = self.make_request(
45
47
  endpoint=f"simple/{package}/",
46
- expected_status_codes=[200, 404],
48
+ expected_status_codes=_HAS_VERSION_STATUS_CODES,
47
49
  fatal_if_unexpected=False,
48
50
  quiet=True,
49
51
  )
@@ -1,15 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
4
-
5
3
  from wexample_helpers.decorator.base_class import base_class
6
4
  from wexample_wex_addon_app.config_value.app_readme_config_value import (
7
5
  AppReadmeConfigValue,
8
6
  )
9
7
 
10
- if TYPE_CHECKING:
11
- pass
12
-
13
8
 
14
9
  @base_class
15
10
  class PythonPackageReadmeContentConfigValue(AppReadmeConfigValue):
@@ -22,9 +17,8 @@ class PythonPackageReadmeContentConfigValue(AppReadmeConfigValue):
22
17
  def _get_app_homepage(self) -> str:
23
18
  """Extract homepage URL from pyproject.toml."""
24
19
  project = self.workdir.get_app_config()
25
- urls = (
26
- project.get("urls", {}) if isinstance(project.get("urls", {}), dict) else {}
27
- )
20
+ raw_urls = project.get("urls", {})
21
+ urls = raw_urls if isinstance(raw_urls, dict) else {}
28
22
  return urls.get("homepage") or urls.get("Homepage") or ""
29
23
 
30
24
  def _get_project_license(self) -> str | None:
@@ -29,13 +29,14 @@ class PythonAppImlFile(ImlFile):
29
29
  return ({"@type": "sourceFolder", "@forTests": "false"},)
30
30
 
31
31
  def _default_source_folders(self) -> Iterable[dict[str, Any]]:
32
+ base = self.MODULE_DIR_URL
32
33
  return (
33
34
  {
34
- "@url": f"{self.MODULE_DIR_URL}/src",
35
+ "@url": f"{base}/src",
35
36
  "@isTestSource": "false",
36
37
  },
37
38
  {
38
- "@url": f"{self.MODULE_DIR_URL}/tests",
39
+ "@url": f"{base}/tests",
39
40
  "@isTestSource": "true",
40
41
  },
41
42
  )
@@ -105,7 +105,7 @@ class PythonPyprojectTomlFile(AppDependenciesConfigFileMixin, TomlFile):
105
105
  deps = self._get_deps_array(optional=optional, group=group)
106
106
 
107
107
  map = {}
108
- for spec in list(deps):
108
+ for spec in deps:
109
109
  req = Requirement(spec)
110
110
  # name: version
111
111
  map[canonicalize_name(req.name)] = str(req.specifier)
@@ -140,7 +140,7 @@ class PythonPyprojectTomlFile(AppDependenciesConfigFileMixin, TomlFile):
140
140
 
141
141
  target = canonicalize_name(package_name)
142
142
  filtered: list[str] = []
143
- for existing in list(deps):
143
+ for existing in deps:
144
144
  try:
145
145
  existing_name = canonicalize_name(Requirement(str(existing)).name)
146
146
  except Exception:
@@ -298,12 +298,12 @@ class PythonPyprojectTomlFile(AppDependenciesConfigFileMixin, TomlFile):
298
298
  deps_arr = self._dependencies_array()
299
299
 
300
300
  runtime_pkgs = {
301
- package_normalize_name(toml_get_string_value(it)) for it in list(deps_arr)
301
+ package_normalize_name(toml_get_string_value(it)) for it in deps_arr
302
302
  }
303
- dev_values = [toml_get_string_value(it).strip() for it in list(dev_arr)]
303
+ dev_set = {toml_get_string_value(it).strip() for it in dev_arr}
304
304
 
305
305
  for pkg in ["pytest", "pytest-cov"]:
306
- if pkg not in runtime_pkgs and pkg not in dev_values:
306
+ if pkg not in runtime_pkgs and pkg not in dev_set:
307
307
  dev_arr.append(pkg)
308
308
 
309
309
  toml_sort_string_array(dev_arr)
@@ -326,7 +326,6 @@ class PythonPyprojectTomlFile(AppDependenciesConfigFileMixin, TomlFile):
326
326
  )
327
327
 
328
328
  deps_arr = self._dependencies_array()
329
- toml_sort_string_array(deps_arr)
330
329
 
331
330
  # Read the keep list from [tool.filestate].keep
332
331
  keep_packages: set[str] = set()
@@ -390,8 +389,9 @@ class PythonPyprojectTomlFile(AppDependenciesConfigFileMixin, TomlFile):
390
389
  existing_keys = list(d.keys())
391
390
 
392
391
  # Build the new order: ordered keys first, then remaining keys
392
+ key_order_set = frozenset(key_order)
393
393
  ordered_keys = [k for k in key_order if k in existing_keys]
394
- remaining_keys = [k for k in existing_keys if k not in key_order]
394
+ remaining_keys = [k for k in existing_keys if k not in key_order_set]
395
395
  new_order = ordered_keys + remaining_keys
396
396
 
397
397
  # Reorder by removing and re-adding in the desired order
@@ -4,8 +4,9 @@ from __future__ import annotations
4
4
  def apply_pdm_bin_dir(value: str) -> None:
5
5
  import os
6
6
 
7
- if value not in os.environ.get("PATH", ""):
8
- os.environ["PATH"] = f"{value}:{os.environ.get('PATH', '')}"
7
+ path = os.environ.get("PATH", "")
8
+ if value not in path:
9
+ os.environ["PATH"] = f"{value}:{path}"
9
10
 
10
11
 
11
12
  def detect_pdm_bin_dir() -> str | None:
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  from typing import TYPE_CHECKING
4
5
 
5
6
  from wexample_wex_core.middleware.each_file_middleware import EachFileMiddleware
@@ -32,12 +33,13 @@ class EachPythonFileMiddleware(EachFileMiddleware):
32
33
  python_extension_only: bool = True
33
34
 
34
35
  def __init__(self, **kwargs) -> None:
35
- # Allow overriding the default settings
36
- if "python_extension_only" in kwargs:
37
- self.python_extension_only = kwargs.pop("python_extension_only")
38
-
39
- if "ignored_directories" in kwargs:
40
- self.ignored_directories = set(kwargs.pop("ignored_directories"))
36
+ # Allow overriding the default settings (pop avoids a double-lookup)
37
+ self.python_extension_only = kwargs.pop(
38
+ "python_extension_only", self.python_extension_only
39
+ )
40
+ ignored = kwargs.pop("ignored_directories", None)
41
+ if ignored is not None:
42
+ self.ignored_directories = set(ignored)
41
43
 
42
44
  super().__init__(**kwargs)
43
45
 
@@ -65,13 +67,9 @@ class EachPythonFileMiddleware(EachFileMiddleware):
65
67
  Returns:
66
68
  True if the item should be processed, False otherwise
67
69
  """
68
- # First check if it's a file (parent class behavior)
69
- if not os.path.isfile(item_path):
70
+ # Cheap string check before the expensive isfile() syscall: skip
71
+ # non-.py paths immediately when extension filtering is active.
72
+ if self.python_extension_only and not item_path.endswith(".py"):
70
73
  return False
71
74
 
72
- # If python_extension_only is enabled, check file extension
73
- if self.python_extension_only:
74
- return item_path.endswith(".py")
75
-
76
- # Otherwise, accept all files
77
- return True
75
+ return os.path.isfile(item_path)
@@ -0,0 +1,7 @@
1
+ """Code-refactor primitives.
2
+
3
+ Step 1: support `kind=package` only. Designed to grow to module / class /
4
+ function / constant / method through the same `SymbolKind` enum.
5
+ """
6
+
7
+ from __future__ import annotations