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.
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/PKG-INFO +4 -4
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/README.md +2 -2
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/pyproject.toml +2 -2
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/common/docstring.py +25 -22
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/common/type_normalizer.py +24 -12
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/class_config.py +3 -10
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/class_method_config.py +3 -3
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/class_property_config.py +2 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/constant_config.py +0 -2
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/function_config.py +0 -8
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/function_parameter_config.py +12 -11
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/generator_config.py +1 -1
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/method_parameter_config.py +2 -1
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/generator/abstract_generator.py +6 -1
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/generator/code_generator.py +11 -14
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/generator/pseudocode_generator.py +11 -17
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/parser/class_parser.py +9 -3
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/parser/function_parser.py +1 -12
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/parser/module_parser.py +5 -4
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/__init__.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/common/__init__.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/__init__.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/doc_comment_config.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/doc_comment_parameter_config.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/doc_comment_return_config.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/generator/__init__.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/parser/__init__.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/py.typed +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/conftest.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/__init__.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/__init__.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/class/__init__.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/class/basic_calculator.py.txt +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/class/basic_calculator.yml +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/constant/__init__.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/constant/constant_using_const.py.txt +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/constant/constant_using_const.yml +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/function/__init__.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/function/basic_function.py.txt +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/function/basic_function.yml +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/function/complex_function.py.txt +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/function/complex_function.yml +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/test_class_basic_calculator.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/test_code_generator_class_basic_calculator.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/test_code_generator_constant.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/test_constant_using_const.py +0 -0
- {wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/test_function_items.py +0 -0
- {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.
|
|
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>=
|
|
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.
|
|
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: >=
|
|
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.
|
|
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: >=
|
|
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.
|
|
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>=
|
|
23
|
+
"wexample-helpers>=17.0.0",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
[project.readme]
|
{wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/common/docstring.py
RENAMED
|
@@ -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.
|
|
22
|
+
lines = [l.strip() for l in doc.splitlines()]
|
|
23
23
|
|
|
24
|
-
#
|
|
25
|
-
|
|
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
|
-
#
|
|
35
|
-
def parse_block(header: str) -> None:
|
|
36
|
-
|
|
37
|
-
|
|
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 =
|
|
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]
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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[
|
|
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[
|
|
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
|
|
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
|
|
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
|
|
38
|
-
return
|
|
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
|
|
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(
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
43
|
+
has_default
|
|
42
44
|
and self.default is None
|
|
43
45
|
and not py_type.startswith("typing.Optional[")
|
|
44
46
|
):
|
|
45
|
-
annotated = f"{
|
|
47
|
+
annotated = f"{name}: typing.Optional[{py_type}]"
|
|
46
48
|
else:
|
|
47
|
-
annotated = f"{
|
|
49
|
+
annotated = f"{name}: {py_type}"
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
if self.has_default:
|
|
51
|
+
if has_default:
|
|
51
52
|
if self.default is None:
|
|
52
|
-
return f"{
|
|
53
|
-
return f"{
|
|
54
|
-
return
|
|
53
|
+
return f"{annotated} = None"
|
|
54
|
+
return f"{annotated} = {_format_value(self.default)}"
|
|
55
|
+
return annotated
|
|
@@ -11,4 +11,5 @@ class MethodParameterConfig:
|
|
|
11
11
|
type: str | None = None
|
|
12
12
|
|
|
13
13
|
def to_code(self) -> str:
|
|
14
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
|
134
|
-
rd["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=
|
|
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=
|
|
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
|
-
|
|
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().
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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]
|
|
File without changes
|
{wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/common/__init__.py
RENAMED
|
File without changes
|
{wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/config/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/src/wexample_pseudocode/parser/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/class/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/constant/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/resources/item/function/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/test_class_basic_calculator.py
RENAMED
|
File without changes
|
|
File without changes
|
{wexample_pseudocode-9.2.0 → wexample_pseudocode-9.3.0}/tests/test_code_generator_constant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|