wexample-pseudocode 9.2.0__tar.gz → 9.3.0__tar.gz

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 (48) hide show
  1. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/PKG-INFO +4 -4
  2. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/README.md +2 -2
  3. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/pyproject.toml +2 -2
  4. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/common/docstring.py +25 -22
  5. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/common/type_normalizer.py +24 -12
  6. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/class_config.py +3 -10
  7. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/class_method_config.py +3 -3
  8. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/class_property_config.py +2 -0
  9. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/constant_config.py +0 -2
  10. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/function_config.py +0 -8
  11. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/function_parameter_config.py +12 -11
  12. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/generator_config.py +1 -1
  13. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/method_parameter_config.py +2 -1
  14. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/generator/abstract_generator.py +6 -1
  15. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/generator/code_generator.py +11 -14
  16. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/generator/pseudocode_generator.py +11 -17
  17. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/parser/class_parser.py +9 -3
  18. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/parser/function_parser.py +1 -12
  19. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/parser/module_parser.py +5 -4
  20. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/__init__.py +0 -0
  21. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/common/__init__.py +0 -0
  22. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/__init__.py +0 -0
  23. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/doc_comment_config.py +0 -0
  24. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/doc_comment_parameter_config.py +0 -0
  25. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/doc_comment_return_config.py +0 -0
  26. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/generator/__init__.py +0 -0
  27. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/parser/__init__.py +0 -0
  28. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/py.typed +0 -0
  29. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/conftest.py +0 -0
  30. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/__init__.py +0 -0
  31. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/__init__.py +0 -0
  32. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/class/__init__.py +0 -0
  33. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/class/basic_calculator.py.txt +0 -0
  34. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/class/basic_calculator.yml +0 -0
  35. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/constant/__init__.py +0 -0
  36. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/constant/constant_using_const.py.txt +0 -0
  37. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/constant/constant_using_const.yml +0 -0
  38. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/function/__init__.py +0 -0
  39. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/function/basic_function.py.txt +0 -0
  40. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/function/basic_function.yml +0 -0
  41. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/function/complex_function.py.txt +0 -0
  42. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/function/complex_function.yml +0 -0
  43. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/test_class_basic_calculator.py +0 -0
  44. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/test_code_generator_class_basic_calculator.py +0 -0
  45. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/test_code_generator_constant.py +0 -0
  46. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/test_constant_using_const.py +0 -0
  47. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/test_function_items.py +0 -0
  48. {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/test_php_yaml_compat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wexample-pseudocode
3
- Version: 9.2.0
3
+ Version: 9.3.0
4
4
  Summary: Code creation and maintenance tools
5
5
  Author-Email: weeger <contact@wexample.com>
6
6
  License: MIT
@@ -11,7 +11,7 @@ Project-URL: homepage, https://github.com/wexample/python-pseudocode
11
11
  Requires-Python: >=3.10
12
12
  Requires-Dist: attrs>=23.1.0
13
13
  Requires-Dist: cattrs>=23.1.0
14
- Requires-Dist: wexample-helpers>=16.2.0
14
+ Requires-Dist: wexample-helpers>=17.0.0
15
15
  Provides-Extra: dev
16
16
  Requires-Dist: pytest; extra == "dev"
17
17
  Requires-Dist: pytest-cov; extra == "dev"
@@ -19,7 +19,7 @@ Description-Content-Type: text/markdown
19
19
 
20
20
  # pseudocode
21
21
 
22
- Version: 9.2.0
22
+ Version: 9.3.0
23
23
 
24
24
  Code creation and maintenance tools
25
25
 
@@ -101,7 +101,7 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
101
101
 
102
102
  - attrs: >=23.1.0
103
103
  - cattrs: >=23.1.0
104
- - wexample-helpers: >=16.2.0
104
+ - wexample-helpers: >=17.0.0
105
105
 
106
106
  ## Versioning & Compatibility Policy
107
107
 
@@ -1,6 +1,6 @@
1
1
  # pseudocode
2
2
 
3
- Version: 9.2.0
3
+ Version: 9.3.0
4
4
 
5
5
  Code creation and maintenance tools
6
6
 
@@ -82,7 +82,7 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
82
82
 
83
83
  - attrs: >=23.1.0
84
84
  - cattrs: >=23.1.0
85
- - wexample-helpers: >=16.2.0
85
+ - wexample-helpers: >=17.0.0
86
86
 
87
87
  ## Versioning & Compatibility Policy
88
88
 
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "wexample-pseudocode"
9
- version = "9.2.0"
9
+ version = "9.3.0"
10
10
  description = "Code creation and maintenance tools"
11
11
  authors = [
12
12
  { name = "weeger", email = "contact@wexample.com" },
@@ -20,7 +20,7 @@ classifiers = [
20
20
  dependencies = [
21
21
  "attrs>=23.1.0",
22
22
  "cattrs>=23.1.0",
23
- "wexample-helpers>=16.2.0",
23
+ "wexample-helpers>=17.0.0",
24
24
  ]
25
25
 
26
26
  [project.readme]
@@ -19,10 +19,11 @@ def parse_docstring(doc: str | None) -> dict[str, dict[str, str]]:
19
19
  if not doc:
20
20
  return result
21
21
 
22
- lines = [l.rstrip() for l in doc.splitlines()]
22
+ lines = [l.strip() for l in doc.splitlines()]
23
23
 
24
- # Pass 1: reST style
25
- for line in lines:
24
+ # Single pass: reST directives + block-header index collection
25
+ header_map: dict[str, int] = {}
26
+ for idx, line in enumerate(lines):
26
27
  m = PARAM_RE.match(line)
27
28
  if m:
28
29
  result["params"][m.group("name")] = m.group("desc").strip()
@@ -30,21 +31,24 @@ def parse_docstring(doc: str | None) -> dict[str, dict[str, str]]:
30
31
  m = RETURN_RE.match(line)
31
32
  if m:
32
33
  result["return"]["description"] = m.group("desc").strip()
34
+ continue
35
+ low = line.lower()
36
+ if low.startswith("args:"):
37
+ header_map.setdefault("args:", idx)
38
+ elif low.startswith("parameters:"):
39
+ header_map.setdefault("parameters:", idx)
40
+ elif low.startswith("returns:"):
41
+ header_map.setdefault("returns:", idx)
33
42
 
34
- # Pass 2: Google/NumPy minimal blocks
35
- def parse_block(header: str) -> None:
36
- try:
37
- idx = next(
38
- i for i, l in enumerate(lines) if l.strip().lower().startswith(header)
39
- )
40
- except StopIteration:
43
+ # Google/NumPy minimal blocks
44
+ def parse_block(header: str, is_params: bool) -> None:
45
+ start = header_map.get(header)
46
+ if start is None:
41
47
  return
42
- i = idx + 1
48
+ i = start + 1
43
49
  n = len(lines)
44
- is_params = header.startswith("args") or header.startswith("parameters")
45
- is_returns = header.startswith("returns")
46
50
  while i < n:
47
- s = lines[i].strip()
51
+ s = lines[i] # already stripped
48
52
  if not s:
49
53
  i += 1
50
54
  continue
@@ -58,15 +62,14 @@ def parse_docstring(doc: str | None) -> dict[str, dict[str, str]]:
58
62
  if is_params:
59
63
  if name:
60
64
  result["params"][name] = desc
61
- elif is_returns:
62
- if desc and "description" not in result["return"]:
63
- result["return"]["description"] = (
64
- f"{name}: {desc}" if name else desc
65
- )
65
+ elif desc and "description" not in result["return"]:
66
+ result["return"]["description"] = (
67
+ f"{name}: {desc}" if name else desc
68
+ )
66
69
  i += 1
67
70
 
68
- parse_block("args:")
69
- parse_block("parameters:")
70
- parse_block("returns:")
71
+ parse_block("args:", True)
72
+ parse_block("parameters:", True)
73
+ parse_block("returns:", False)
71
74
 
72
75
  return result
@@ -1,5 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
+ _TYPING_PREFIX_LEN: int = 7 # len("typing.")
4
+ _OPTIONAL_PREFIX_LEN: int = 9 # len("Optional[")
5
+
6
+ _ARRAY_TYPES: frozenset[str] = frozenset(
7
+ {"list", "List", "Sequence", "MutableSequence", "dict", "Dict", "Mapping"}
8
+ )
9
+ _PRIMITIVE_TYPES: frozenset[str] = frozenset(
10
+ {"int", "float", "str", "bool", "mixed", "void", "None"}
11
+ )
12
+
13
+ # Single-lookup table used by to_python_type; "void" maps to None intentionally.
14
+ _TO_PYTHON: dict[str, str | None] = {
15
+ "array": "list",
16
+ "callable": "typing.Callable",
17
+ "mixed": "typing.Any",
18
+ "void": None,
19
+ }
20
+
3
21
 
4
22
  def normalize_type(type_str: str | None) -> str | None:
5
23
  if type_str is None:
@@ -8,23 +26,23 @@ def normalize_type(type_str: str | None) -> str | None:
8
26
 
9
27
  # Strip leading 'typing.' for readability
10
28
  if t.startswith("typing."):
11
- t = t[len("typing.") :]
29
+ t = t[_TYPING_PREFIX_LEN:]
12
30
 
13
31
  # Optional[...] -> inner + marker handled by caller
14
32
  if t.startswith("Optional[") and t.endswith("]"):
15
- inner = t[len("Optional[") : -1].strip()
33
+ inner = t[_OPTIONAL_PREFIX_LEN:-1].strip()
16
34
  return normalize_type(inner)
17
35
 
18
36
  # Common aliases across languages
19
37
  # Python -> schema (PHP uses 'array')
20
- if t in {"list", "List", "Sequence", "MutableSequence", "dict", "Dict", "Mapping"}:
38
+ if t in _ARRAY_TYPES:
21
39
  return "array"
22
40
 
23
41
  if t == "Callable":
24
42
  return "callable"
25
43
 
26
44
  # Primitives stay as-is
27
- if t in {"int", "float", "str", "bool", "mixed", "void", "None"}:
45
+ if t in _PRIMITIVE_TYPES:
28
46
  return t if t != "None" else None
29
47
 
30
48
  return t
@@ -34,12 +52,6 @@ def to_python_type(type_str: str | None) -> str | None:
34
52
  if type_str is None:
35
53
  return None
36
54
  t = type_str.strip()
37
- if t == "array":
38
- return "list"
39
- if t == "callable":
40
- return "typing.Callable"
41
- if t == "mixed":
42
- return "typing.Any"
43
- if t == "void":
44
- return None
55
+ if t in _TO_PYTHON:
56
+ return _TO_PYTHON[t]
45
57
  return t
@@ -1,23 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
- from typing import TYPE_CHECKING, Any
5
-
6
- from wexample_pseudocode.config.generator_config import GeneratorConfig
4
+ from typing import TYPE_CHECKING
7
5
 
8
6
  if TYPE_CHECKING:
7
+ from typing import Any
8
+
9
9
  from wexample_pseudocode.config.class_method_config import ClassMethodConfig
10
10
  from wexample_pseudocode.config.class_property_config import ClassPropertyConfig
11
11
  from wexample_pseudocode.config.generator_config import GeneratorConfig
12
12
 
13
13
 
14
- def _format_value(value: Any) -> str:
15
- if isinstance(value, str):
16
- escaped = value.replace('"', '\\"')
17
- return f'"{escaped}"'
18
- return repr(value)
19
-
20
-
21
14
  @dataclass
22
15
  class ClassConfig:
23
16
  name: str
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
+ from itertools import chain
4
5
  from typing import TYPE_CHECKING, Any
5
6
 
6
7
  if TYPE_CHECKING:
@@ -42,7 +43,7 @@ class ClassMethodConfig:
42
43
  def to_code(self, indent: str = " ") -> str:
43
44
  from wexample_pseudocode.common.type_normalizer import to_python_type
44
45
 
45
- params_src = ", ".join(["self"] + [p.to_code() for p in self.parameters])
46
+ params_src = ", ".join(chain(("self",), (p.to_code() for p in self.parameters)))
46
47
  py_ret = to_python_type(self.return_type)
47
48
  ret = f" -> {py_ret}" if py_ret else ""
48
49
  header = f"def {self.name}({params_src}){ret}:"
@@ -67,8 +68,7 @@ class ClassMethodConfig:
67
68
  if doc_lines:
68
69
  first = doc_lines[0]
69
70
  rest = doc_lines[1:]
70
- inner_block: list[str] = []
71
- inner_block.append(first)
71
+ inner_block: list[str] = [first]
72
72
  if rest:
73
73
  inner_block.append("") # blank line before param/return block
74
74
  # indent param/return lines inside the docstring
@@ -6,6 +6,8 @@ from typing import Any
6
6
 
7
7
  def _format_value(value: Any) -> str:
8
8
  if isinstance(value, str):
9
+ if '"' not in value:
10
+ return f'"{value}"'
9
11
  escaped = value.replace('"', '\\"')
10
12
  return f'"{escaped}"'
11
13
  return repr(value)
@@ -3,8 +3,6 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
- from wexample_pseudocode.config.generator_config import GeneratorConfig
7
-
8
6
  if TYPE_CHECKING:
9
7
  from wexample_pseudocode.config.generator_config import GeneratorConfig
10
8
 
@@ -9,14 +9,6 @@ if TYPE_CHECKING:
9
9
  from wexample_pseudocode.config.function_parameter_config import (
10
10
  FunctionParameterConfig,
11
11
  )
12
- from wexample_pseudocode.config.generator_config import GeneratorConfig
13
-
14
-
15
- def _format_value(value: Any) -> str:
16
- if isinstance(value, str):
17
- escaped = value.replace('"', '\\"')
18
- return f'"{escaped}"'
19
- return repr(value)
20
12
 
21
13
 
22
14
  @dataclass
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import Any
5
5
 
6
+ from wexample_pseudocode.common.type_normalizer import to_python_type
7
+
6
8
 
7
9
  def _format_value(value: Any) -> str:
8
10
  if isinstance(value, str):
@@ -31,24 +33,23 @@ class FunctionParameterConfig:
31
33
  )
32
34
 
33
35
  def to_code(self) -> str:
34
- from wexample_pseudocode.common.type_normalizer import to_python_type
35
-
36
36
  py_type = to_python_type(self.type)
37
- annotated = self.name
37
+ name = self.name
38
+ has_default = self.has_default
39
+ annotated = name
38
40
  if py_type is not None:
39
41
  # If default is None (optional), wrap type with typing.Optional[]
40
42
  if (
41
- self.has_default
43
+ has_default
42
44
  and self.default is None
43
45
  and not py_type.startswith("typing.Optional[")
44
46
  ):
45
- annotated = f"{self.name}: typing.Optional[{py_type}]"
47
+ annotated = f"{name}: typing.Optional[{py_type}]"
46
48
  else:
47
- annotated = f"{self.name}: {py_type}"
49
+ annotated = f"{name}: {py_type}"
48
50
 
49
- left = annotated
50
- if self.has_default:
51
+ if has_default:
51
52
  if self.default is None:
52
- return f"{left} = None"
53
- return f"{left} = {_format_value(self.default)}"
54
- return left
53
+ return f"{annotated} = None"
54
+ return f"{annotated} = {_format_value(self.default)}"
55
+ return annotated
@@ -4,7 +4,7 @@ from dataclasses import dataclass
4
4
  from typing import Any
5
5
 
6
6
 
7
- @dataclass
7
+ @dataclass(slots=True)
8
8
  class GeneratorConfig:
9
9
  """Global generator config (minimal placeholder).
10
10
 
@@ -11,4 +11,5 @@ class MethodParameterConfig:
11
11
  type: str | None = None
12
12
 
13
13
  def to_code(self) -> str:
14
- return f"{self.name}: {self.type}" if self.type is not None else self.name
14
+ name, param_type = self.name, self.type
15
+ return f"{name}: {param_type}" if param_type is not None else name
@@ -1,10 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import functools
3
4
  from typing import Any
4
5
 
5
6
  import yaml
6
7
  from wexample_helpers.classes.abstract_method import abstract_method
7
8
 
9
+ # Pre-bind fixed kwargs so dump_pseudocode avoids re-allocating the kwargs
10
+ # dict on every call.
11
+ _yaml_dump = functools.partial(yaml.safe_dump, sort_keys=False, allow_unicode=True)
12
+
8
13
 
9
14
  class AbstractGenerator:
10
15
  """Base class mirroring the PHP AbstractGenerator responsibilities.
@@ -19,7 +24,7 @@ class AbstractGenerator:
19
24
 
20
25
  Kept as a static method to mimic the PHP usage ergonomics.
21
26
  """
22
- return yaml.safe_dump(data, sort_keys=False, allow_unicode=True)
27
+ return _yaml_dump(data)
23
28
 
24
29
  @abstract_method
25
30
  def generate_config_data(self, source_code: str) -> dict[str, Any]:
@@ -33,7 +33,7 @@ class CodeGenerator(AbstractGenerator):
33
33
 
34
34
  def generate(self, input_text: str) -> str:
35
35
  configs = self._generate_config(input_text)
36
- return "".join(cfg.to_code() + "\n" for cfg in configs)
36
+ return "\n".join(cfg.to_code() for cfg in configs) + "\n" if configs else ""
37
37
 
38
38
  # Implement abstract method (not used directly in CodeGenerator flow)
39
39
  def generate_config_data(self, source_code: str) -> dict[str, Any]:
@@ -57,16 +57,13 @@ class CodeGenerator(AbstractGenerator):
57
57
  from wexample_pseudocode.config.generator_config import GeneratorConfig
58
58
 
59
59
  data = yaml.safe_load(input_text) or {}
60
- instances = []
61
-
62
- global_generator_config = None
63
- if "generator" in data:
64
- global_generator_config = GeneratorConfig.from_config(
65
- data["generator"]
66
- ) # kept for parity
67
-
68
- for item in data.get("items", []) or []:
69
- config_cls = self._find_config_loader(item)
70
- if config_cls is not None:
71
- instances.append(config_cls.from_config(item, global_generator_config))
72
- return instances
60
+
61
+ global_generator_config = (
62
+ GeneratorConfig.from_config(data["generator"]) if "generator" in data else None
63
+ )
64
+
65
+ return [
66
+ config_cls.from_config(item, global_generator_config)
67
+ for item in data.get("items", []) or []
68
+ if (config_cls := self._find_config_loader(item)) is not None
69
+ ]
@@ -1,22 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import ast as _ast
3
4
  from dataclasses import dataclass
4
5
  from typing import Any
5
6
 
7
+ from wexample_pseudocode.common.type_normalizer import normalize_type
6
8
  from wexample_pseudocode.generator.abstract_generator import AbstractGenerator
9
+ from wexample_pseudocode.parser.class_parser import parse_module_classes
10
+ from wexample_pseudocode.parser.function_parser import parse_module_functions
11
+ from wexample_pseudocode.parser.module_parser import parse_module_constants
7
12
 
8
13
 
9
14
  def _literal_eval_safe(node):
15
+ if node is None:
16
+ return None
10
17
  try:
11
- import ast as _ast
12
-
13
- if node is None:
14
- return None
15
18
  return _ast.literal_eval(node)
16
19
  except Exception:
17
20
  try:
18
- import ast as _ast
19
-
20
21
  return _ast.unparse(node) # type: ignore[attr-defined]
21
22
  except Exception:
22
23
  return None
@@ -32,11 +33,6 @@ class PseudocodeGenerator(AbstractGenerator):
32
33
  """
33
34
 
34
35
  def generate_config_data(self, source_code: str) -> dict[str, Any]:
35
- from wexample_pseudocode.common.type_normalizer import normalize_type
36
- from wexample_pseudocode.parser.class_parser import parse_module_classes
37
- from wexample_pseudocode.parser.function_parser import parse_module_functions
38
- from wexample_pseudocode.parser.module_parser import parse_module_constants
39
-
40
36
  items: list[dict[str, Any]] = []
41
37
 
42
38
  for const in parse_module_constants(source_code):
@@ -123,15 +119,13 @@ class PseudocodeGenerator(AbstractGenerator):
123
119
  pd["default"] = default_val
124
120
  params.append(pd)
125
121
  item["parameters"] = params
126
- if (
127
- fn.return_type is not None
128
- or getattr(fn, "return_description", None) is not None
129
- ):
122
+ fn_return_desc = getattr(fn, "return_description", None)
123
+ if fn.return_type is not None or fn_return_desc is not None:
130
124
  rd: dict[str, Any] = {}
131
125
  if fn.return_type is not None:
132
126
  rd["type"] = normalize_type(fn.return_type)
133
- if getattr(fn, "return_description", None):
134
- rd["description"] = fn.return_description
127
+ if fn_return_desc:
128
+ rd["description"] = fn_return_desc
135
129
  item["return"] = rd
136
130
  items.append(item)
137
131
 
@@ -58,12 +58,16 @@ def parse_module_classes(source_code: str) -> Iterable[ClassItem]:
58
58
  continue
59
59
  raw_doc = ast.get_docstring(stmt)
60
60
  parsed = parse_docstring(raw_doc)
61
+ # hoist dict lookups out of per-arg / per-method loops
62
+ return_info = parsed.get("return")
63
+ return_desc = return_info.get("description") if return_info else None
61
64
  m = ClassMethod(
62
65
  name=stmt.name,
63
66
  description=_first_line(raw_doc),
64
67
  return_type=_annotation_to_str(stmt.returns),
65
- return_description=parsed.get("return", {}).get("description"),
68
+ return_description=return_desc,
66
69
  )
70
+ params_dict = parsed.get("params") or {}
67
71
  for arg in stmt.args.args:
68
72
  if arg.arg == "self":
69
73
  continue
@@ -71,7 +75,7 @@ def parse_module_classes(source_code: str) -> Iterable[ClassItem]:
71
75
  MethodParameter(
72
76
  name=arg.arg,
73
77
  type=_annotation_to_str(arg.annotation),
74
- description=parsed.get("params", {}).get(arg.arg),
78
+ description=params_dict.get(arg.arg),
75
79
  )
76
80
  )
77
81
  cls.methods.append(m)
@@ -92,7 +96,9 @@ def _annotation_to_str(ann: ast.AST | None) -> str | None:
92
96
  def _first_line(doc: str | None) -> str | None:
93
97
  if not doc:
94
98
  return None
95
- return doc.strip().splitlines()[0].strip()
99
+ # partition stops at the first '\n' without building a full list
100
+ first, _, _ = doc.lstrip().partition('\n')
101
+ return first.rstrip()
96
102
 
97
103
 
98
104
  def _literal_eval_safe(node: ast.AST | None):
@@ -14,17 +14,6 @@ def parse_module_functions(source_code: str) -> Iterable[FunctionItem]:
14
14
 
15
15
  tree = ast.parse(source_code)
16
16
 
17
- def _literal(node: ast.AST | None):
18
- if node is None:
19
- return None
20
- try:
21
- return ast.literal_eval(node)
22
- except Exception:
23
- try:
24
- return ast.unparse(node) # type: ignore[attr-defined]
25
- except Exception:
26
- return None
27
-
28
17
  for node in tree.body:
29
18
  if isinstance(node, ast.FunctionDef):
30
19
  raw_doc = ast.get_docstring(node)
@@ -61,7 +50,7 @@ def parse_module_functions(source_code: str) -> Iterable[FunctionItem]:
61
50
  def _first_line(doc: str | None) -> str | None:
62
51
  if not doc:
63
52
  return None
64
- return doc.strip().splitlines()[0].strip()
53
+ return doc.strip().partition('\n')[0].strip()
65
54
 
66
55
 
67
56
  @dataclass
@@ -21,9 +21,10 @@ def parse_module_constants(source_code: str) -> Iterable[ConstantItem]:
21
21
  # Python's ast doesn't keep comments; we do a light heuristic by reading the source.
22
22
  line_map = {}
23
23
  for i, line in enumerate(source_code.splitlines(), start=1):
24
- if "#" in line:
25
- # take text after the first #, strip spaces
26
- comment = line.split("#", 1)[1].strip()
24
+ # partition does a single scan; avoids the double-pass of `in` + split
25
+ _, sep, rest = line.partition("#")
26
+ if sep:
27
+ comment = rest.strip()
27
28
  if comment:
28
29
  line_map[i] = comment
29
30
 
@@ -48,7 +49,7 @@ def parse_module_constants(source_code: str) -> Iterable[ConstantItem]:
48
49
  def _literal_eval_safe(node: ast.AST):
49
50
  try:
50
51
  return ast.literal_eval(node)
51
- except Exception:
52
+ except (ValueError, TypeError):
52
53
  # Fallback to a simple repr if non-literal
53
54
  try:
54
55
  return ast.unparse(node) # type: ignore[attr-defined]