tywrap-ir 0.1.2__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.
- tywrap_ir-0.1.2/PKG-INFO +71 -0
- tywrap_ir-0.1.2/README.md +45 -0
- tywrap_ir-0.1.2/pyproject.toml +45 -0
- tywrap_ir-0.1.2/setup.cfg +4 -0
- tywrap_ir-0.1.2/tywrap_ir/__init__.py +10 -0
- tywrap_ir-0.1.2/tywrap_ir/__main__.py +41 -0
- tywrap_ir-0.1.2/tywrap_ir/ir.py +462 -0
- tywrap_ir-0.1.2/tywrap_ir/optimized_ir.py +544 -0
- tywrap_ir-0.1.2/tywrap_ir.egg-info/PKG-INFO +71 -0
- tywrap_ir-0.1.2/tywrap_ir.egg-info/SOURCES.txt +11 -0
- tywrap_ir-0.1.2/tywrap_ir.egg-info/dependency_links.txt +1 -0
- tywrap_ir-0.1.2/tywrap_ir.egg-info/entry_points.txt +2 -0
- tywrap_ir-0.1.2/tywrap_ir.egg-info/top_level.txt +1 -0
tywrap_ir-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tywrap-ir
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Python IR extractor for tywrap: emits versioned JSON IR for Python modules
|
|
5
|
+
Author: tywrap contributors
|
|
6
|
+
Maintainer: tywrap contributors
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/bbopen/tywrap
|
|
9
|
+
Project-URL: Repository, https://github.com/bbopen/tywrap
|
|
10
|
+
Project-URL: Documentation, https://github.com/bbopen/tywrap#readme
|
|
11
|
+
Project-URL: Issues, https://github.com/bbopen/tywrap/issues
|
|
12
|
+
Keywords: tywrap,typescript,python,bridge,code-generation,ast,ir
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# tywrap-ir
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/tywrap-ir/)
|
|
30
|
+
[](https://pypi.org/project/tywrap-ir/)
|
|
31
|
+
[](https://opensource.org/licenses/MIT)
|
|
32
|
+
|
|
33
|
+
Python IR extractor for [tywrap](https://github.com/bbopen/tywrap). Emits versioned JSON IR for Python modules using inspect/typing/importlib.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install tywrap-ir
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Extract IR for a module
|
|
45
|
+
python -m tywrap_ir --module math
|
|
46
|
+
|
|
47
|
+
# Or using the CLI
|
|
48
|
+
tywrap-ir --module math
|
|
49
|
+
|
|
50
|
+
# Output to file
|
|
51
|
+
tywrap-ir --module pandas --output pandas_ir.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## What is this?
|
|
55
|
+
|
|
56
|
+
This package is the Python component of tywrap, a TypeScript wrapper generator for Python libraries. It analyzes Python modules and extracts type information into a JSON intermediate representation (IR) that tywrap uses to generate TypeScript bindings.
|
|
57
|
+
|
|
58
|
+
You typically don't need to use this package directly - the `tywrap` npm package invokes it automatically during code generation.
|
|
59
|
+
|
|
60
|
+
## Requirements
|
|
61
|
+
|
|
62
|
+
- Python 3.10+
|
|
63
|
+
|
|
64
|
+
## Related
|
|
65
|
+
|
|
66
|
+
- [tywrap](https://www.npmjs.com/package/tywrap) - The main TypeScript package
|
|
67
|
+
- [GitHub](https://github.com/bbopen/tywrap) - Source code and issues
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# tywrap-ir
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/tywrap-ir/)
|
|
4
|
+
[](https://pypi.org/project/tywrap-ir/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Python IR extractor for [tywrap](https://github.com/bbopen/tywrap). Emits versioned JSON IR for Python modules using inspect/typing/importlib.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install tywrap-ir
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Extract IR for a module
|
|
19
|
+
python -m tywrap_ir --module math
|
|
20
|
+
|
|
21
|
+
# Or using the CLI
|
|
22
|
+
tywrap-ir --module math
|
|
23
|
+
|
|
24
|
+
# Output to file
|
|
25
|
+
tywrap-ir --module pandas --output pandas_ir.json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## What is this?
|
|
29
|
+
|
|
30
|
+
This package is the Python component of tywrap, a TypeScript wrapper generator for Python libraries. It analyzes Python modules and extracts type information into a JSON intermediate representation (IR) that tywrap uses to generate TypeScript bindings.
|
|
31
|
+
|
|
32
|
+
You typically don't need to use this package directly - the `tywrap` npm package invokes it automatically during code generation.
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- Python 3.10+
|
|
37
|
+
|
|
38
|
+
## Related
|
|
39
|
+
|
|
40
|
+
- [tywrap](https://www.npmjs.com/package/tywrap) - The main TypeScript package
|
|
41
|
+
- [GitHub](https://github.com/bbopen/tywrap) - Source code and issues
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tywrap-ir"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Python IR extractor for tywrap: emits versioned JSON IR for Python modules"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "tywrap contributors" }
|
|
12
|
+
]
|
|
13
|
+
maintainers = [
|
|
14
|
+
{ name = "tywrap contributors" }
|
|
15
|
+
]
|
|
16
|
+
requires-python = ">=3.10"
|
|
17
|
+
dependencies = []
|
|
18
|
+
license = { text = "MIT" }
|
|
19
|
+
keywords = ["tywrap", "typescript", "python", "bridge", "code-generation", "ast", "ir"]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Topic :: Software Development :: Code Generators",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
|
+
"Typing :: Typed",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/bbopen/tywrap"
|
|
36
|
+
Repository = "https://github.com/bbopen/tywrap"
|
|
37
|
+
Documentation = "https://github.com/bbopen/tywrap#readme"
|
|
38
|
+
Issues = "https://github.com/bbopen/tywrap/issues"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
tywrap-ir = "tywrap_ir.__main__:main"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["."]
|
|
45
|
+
include = ["tywrap_ir*"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from .ir import emit_ir_json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
parser = argparse.ArgumentParser(description="tywrap IR extractor")
|
|
10
|
+
parser.add_argument("--module", help="Python module name, e.g. math or pandas")
|
|
11
|
+
parser.add_argument("--package", help="Python package name (alias of --module for now)")
|
|
12
|
+
parser.add_argument("--output", help="Write JSON IR to file instead of stdout")
|
|
13
|
+
parser.add_argument("--ir-version", default="0.1.0", help="IR schema version")
|
|
14
|
+
parser.add_argument("--include-private", action="store_true", help="Include private members (leading _)")
|
|
15
|
+
parser.add_argument("--no-pretty", action="store_true", help="Disable pretty JSON formatting")
|
|
16
|
+
args = parser.parse_args()
|
|
17
|
+
|
|
18
|
+
target = args.module or args.package
|
|
19
|
+
if not target:
|
|
20
|
+
sys.stderr.write("Error: --module or --package is required\n")
|
|
21
|
+
sys.exit(2)
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
output = emit_ir_json(
|
|
25
|
+
target,
|
|
26
|
+
ir_version=args.ir_version,
|
|
27
|
+
include_private=args.include_private,
|
|
28
|
+
pretty=not args.no_pretty,
|
|
29
|
+
)
|
|
30
|
+
if args.output:
|
|
31
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
32
|
+
f.write(output)
|
|
33
|
+
else:
|
|
34
|
+
sys.stdout.write(output + "\n")
|
|
35
|
+
except Exception as exc: # pragma: no cover
|
|
36
|
+
sys.stderr.write(f"Error: {exc}\n")
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
main()
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
import platform
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass, asdict
|
|
9
|
+
from typing import Any, Dict, List, Optional, get_type_hints
|
|
10
|
+
import dataclasses as _dataclasses
|
|
11
|
+
import typing
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from importlib import metadata as importlib_metadata # py3.8+
|
|
15
|
+
except Exception: # pragma: no cover
|
|
16
|
+
import importlib_metadata # type: ignore
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class IRParam:
|
|
21
|
+
name: str
|
|
22
|
+
kind: str
|
|
23
|
+
annotation: str | None
|
|
24
|
+
default: bool
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class IRFunction:
|
|
29
|
+
name: str
|
|
30
|
+
qualname: str
|
|
31
|
+
docstring: Optional[str]
|
|
32
|
+
parameters: List[IRParam]
|
|
33
|
+
returns: Optional[str]
|
|
34
|
+
is_async: bool
|
|
35
|
+
is_generator: bool
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class IRClass:
|
|
40
|
+
name: str
|
|
41
|
+
qualname: str
|
|
42
|
+
docstring: Optional[str]
|
|
43
|
+
bases: List[str]
|
|
44
|
+
methods: List[IRFunction]
|
|
45
|
+
typed_dict: bool
|
|
46
|
+
total: Optional[bool]
|
|
47
|
+
fields: List[IRParam]
|
|
48
|
+
is_protocol: bool
|
|
49
|
+
is_namedtuple: bool
|
|
50
|
+
is_dataclass: bool
|
|
51
|
+
is_pydantic: bool
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class IRConstant:
|
|
56
|
+
name: str
|
|
57
|
+
annotation: str | None
|
|
58
|
+
value_repr: str | None
|
|
59
|
+
is_final: bool
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class IRTypeAlias:
|
|
64
|
+
name: str
|
|
65
|
+
definition: str
|
|
66
|
+
is_generic: bool
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class IRModule:
|
|
71
|
+
ir_version: str
|
|
72
|
+
module: str
|
|
73
|
+
functions: List[IRFunction]
|
|
74
|
+
classes: List[IRClass]
|
|
75
|
+
constants: List[IRConstant]
|
|
76
|
+
type_aliases: List[IRTypeAlias]
|
|
77
|
+
metadata: Dict[str, Any]
|
|
78
|
+
warnings: List[str]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Minimal, stringified annotation representation
|
|
82
|
+
|
|
83
|
+
def _stringify_annotation(annotation: Any) -> Optional[str]:
|
|
84
|
+
if annotation is inspect._empty: # type: ignore[attr-defined]
|
|
85
|
+
return None
|
|
86
|
+
try:
|
|
87
|
+
# Handle forward references more elegantly
|
|
88
|
+
str_repr = str(annotation)
|
|
89
|
+
|
|
90
|
+
# Clean up class references to show just the class name
|
|
91
|
+
if str_repr.startswith("<class '") and str_repr.endswith("'>"):
|
|
92
|
+
# Extract class name from <class 'module.ClassName'>
|
|
93
|
+
class_path = str_repr[8:-2] # Remove <class ' and '>
|
|
94
|
+
if '.' in class_path:
|
|
95
|
+
return class_path.split('.')[-1] # Just the class name
|
|
96
|
+
return class_path
|
|
97
|
+
|
|
98
|
+
return str_repr
|
|
99
|
+
except Exception:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _param_kind_to_str(kind: inspect._ParameterKind) -> str:
|
|
104
|
+
mapping = {
|
|
105
|
+
inspect.Parameter.POSITIONAL_ONLY: "POSITIONAL_ONLY",
|
|
106
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD: "POSITIONAL_OR_KEYWORD",
|
|
107
|
+
inspect.Parameter.VAR_POSITIONAL: "VAR_POSITIONAL",
|
|
108
|
+
inspect.Parameter.KEYWORD_ONLY: "KEYWORD_ONLY",
|
|
109
|
+
inspect.Parameter.VAR_KEYWORD: "VAR_KEYWORD",
|
|
110
|
+
}
|
|
111
|
+
return mapping.get(kind, str(kind))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _extract_constants(module: Any, module_name: str, include_private: bool) -> List[IRConstant]:
|
|
115
|
+
"""Extract module-level constants and Final variables."""
|
|
116
|
+
constants: List[IRConstant] = []
|
|
117
|
+
|
|
118
|
+
# Check module annotations for type hints
|
|
119
|
+
annotations = getattr(module, '__annotations__', {})
|
|
120
|
+
|
|
121
|
+
for name in dir(module):
|
|
122
|
+
if not include_private and name.startswith("_"):
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
value = getattr(module, name)
|
|
127
|
+
except Exception:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
# Skip functions, classes, modules, and other non-constant items
|
|
131
|
+
if (inspect.isfunction(value) or inspect.isclass(value) or
|
|
132
|
+
inspect.ismodule(value) or callable(value)):
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
# Check if it's a constant (uppercase naming convention or Final)
|
|
136
|
+
is_constant = name.isupper() or name in annotations
|
|
137
|
+
|
|
138
|
+
if is_constant:
|
|
139
|
+
annotation = annotations.get(name)
|
|
140
|
+
annotation_str = _stringify_annotation(annotation) if annotation else None
|
|
141
|
+
|
|
142
|
+
# Check if it's Final
|
|
143
|
+
is_final = False
|
|
144
|
+
if annotation_str and ('Final[' in annotation_str or annotation_str == 'Final'):
|
|
145
|
+
is_final = True
|
|
146
|
+
|
|
147
|
+
# Get string representation of value (truncated for large objects)
|
|
148
|
+
try:
|
|
149
|
+
value_repr = repr(value)
|
|
150
|
+
if len(value_repr) > 200:
|
|
151
|
+
value_repr = value_repr[:197] + "..."
|
|
152
|
+
except Exception:
|
|
153
|
+
value_repr = "<unrepresentable>"
|
|
154
|
+
|
|
155
|
+
constants.append(IRConstant(
|
|
156
|
+
name=name,
|
|
157
|
+
annotation=annotation_str,
|
|
158
|
+
value_repr=value_repr,
|
|
159
|
+
is_final=is_final
|
|
160
|
+
))
|
|
161
|
+
|
|
162
|
+
return constants
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _extract_type_aliases(module: Any, module_name: str, include_private: bool) -> List[IRTypeAlias]:
|
|
166
|
+
"""Extract type aliases from module."""
|
|
167
|
+
type_aliases: List[IRTypeAlias] = []
|
|
168
|
+
|
|
169
|
+
# Check module annotations for type aliases
|
|
170
|
+
annotations = getattr(module, '__annotations__', {})
|
|
171
|
+
|
|
172
|
+
for name, annotation in annotations.items():
|
|
173
|
+
if not include_private and name.startswith("_"):
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
value = getattr(module, name, None)
|
|
178
|
+
except Exception:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Type aliases are typically annotated but check for specific patterns
|
|
182
|
+
annotation_str = _stringify_annotation(annotation)
|
|
183
|
+
|
|
184
|
+
# Check if it's a type alias (has TypeAlias annotation or follows patterns)
|
|
185
|
+
is_type_alias = (
|
|
186
|
+
annotation_str and ('TypeAlias' in annotation_str or
|
|
187
|
+
'typing.Union' in annotation_str or
|
|
188
|
+
'typing.Optional' in annotation_str or
|
|
189
|
+
'typing.List' in annotation_str or
|
|
190
|
+
'typing.Dict' in annotation_str or
|
|
191
|
+
'|' in annotation_str) # Modern union syntax
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if is_type_alias:
|
|
195
|
+
# Check if it's generic (contains type parameters)
|
|
196
|
+
is_generic = bool(annotation_str and any(
|
|
197
|
+
marker in annotation_str for marker in ['[', '~', 'TypeVar', 'Generic']
|
|
198
|
+
))
|
|
199
|
+
|
|
200
|
+
type_aliases.append(IRTypeAlias(
|
|
201
|
+
name=name,
|
|
202
|
+
definition=annotation_str or str(annotation),
|
|
203
|
+
is_generic=is_generic
|
|
204
|
+
))
|
|
205
|
+
|
|
206
|
+
return type_aliases
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _extract_function(obj: Any, qualname: str) -> Optional[IRFunction]:
|
|
210
|
+
try:
|
|
211
|
+
sig = inspect.signature(obj)
|
|
212
|
+
except Exception:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
# Use get_type_hints to resolve ForwardRefs where possible
|
|
216
|
+
try:
|
|
217
|
+
hints = get_type_hints(obj)
|
|
218
|
+
except Exception:
|
|
219
|
+
hints = {}
|
|
220
|
+
|
|
221
|
+
params: List[IRParam] = []
|
|
222
|
+
for name, p in sig.parameters.items():
|
|
223
|
+
ann = hints.get(name, p.annotation)
|
|
224
|
+
params.append(
|
|
225
|
+
IRParam(
|
|
226
|
+
name=name,
|
|
227
|
+
kind=_param_kind_to_str(p.kind),
|
|
228
|
+
annotation=_stringify_annotation(ann),
|
|
229
|
+
default=(p.default is not inspect._empty),
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
returns = hints.get("return", sig.return_annotation)
|
|
234
|
+
is_async = inspect.iscoroutinefunction(obj) or inspect.isasyncgenfunction(obj)
|
|
235
|
+
is_generator = inspect.isgeneratorfunction(obj)
|
|
236
|
+
|
|
237
|
+
return IRFunction(
|
|
238
|
+
name=getattr(obj, "__name__", qualname.split(".")[-1]),
|
|
239
|
+
qualname=qualname,
|
|
240
|
+
docstring=inspect.getdoc(obj),
|
|
241
|
+
parameters=params,
|
|
242
|
+
returns=_stringify_annotation(returns),
|
|
243
|
+
is_async=is_async,
|
|
244
|
+
is_generator=is_generator,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _extract_class(cls: type, module_name: str, include_private: bool) -> Optional[IRClass]:
|
|
249
|
+
name = getattr(cls, "__name__", None)
|
|
250
|
+
if not name:
|
|
251
|
+
return None
|
|
252
|
+
if not include_private and name.startswith("_"):
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
bases = [b.__name__ for b in getattr(cls, "__bases__", []) if hasattr(b, "__name__")]
|
|
256
|
+
|
|
257
|
+
methods: List[IRFunction] = []
|
|
258
|
+
for meth_name, value in inspect.getmembers(
|
|
259
|
+
cls,
|
|
260
|
+
predicate=lambda x: inspect.isfunction(x) or inspect.ismethoddescriptor(x) or inspect.isbuiltin(x),
|
|
261
|
+
):
|
|
262
|
+
if not include_private and meth_name.startswith("_"):
|
|
263
|
+
continue
|
|
264
|
+
fn = _extract_function(value, f"{module_name}.{cls.__name__}.{meth_name}")
|
|
265
|
+
if fn is not None:
|
|
266
|
+
methods.append(fn)
|
|
267
|
+
|
|
268
|
+
# TypedDict detection and fields
|
|
269
|
+
typed_dict = False
|
|
270
|
+
total: Optional[bool] = None
|
|
271
|
+
fields: List[IRParam] = []
|
|
272
|
+
try:
|
|
273
|
+
# Heuristic: TypedDict classes have __annotations__ and __total__
|
|
274
|
+
if hasattr(cls, "__annotations__") and hasattr(cls, "__total__"):
|
|
275
|
+
typed_dict = True
|
|
276
|
+
total = bool(getattr(cls, "__total__", True))
|
|
277
|
+
ann = get_type_hints(cls, include_extras=True) if hasattr(typing, "get_origin") else getattr(cls, "__annotations__", {})
|
|
278
|
+
for fname, ftype in ann.items():
|
|
279
|
+
text = _stringify_annotation(ftype)
|
|
280
|
+
# Determine optionality from NotRequired/Required wrappers if present
|
|
281
|
+
s = str(ftype)
|
|
282
|
+
is_not_required = "NotRequired[" in s or "typing.NotRequired[" in s
|
|
283
|
+
is_required = "Required[" in s or "typing.Required[" in s
|
|
284
|
+
optional_flag = is_not_required or (not is_required and total is False)
|
|
285
|
+
fields.append(IRParam(name=fname, kind="FIELD", annotation=text, default=optional_flag))
|
|
286
|
+
except Exception:
|
|
287
|
+
pass
|
|
288
|
+
|
|
289
|
+
# Protocol detection
|
|
290
|
+
is_protocol = False
|
|
291
|
+
try:
|
|
292
|
+
for b in getattr(cls, "__mro__", []):
|
|
293
|
+
if getattr(b, "__name__", None) == "Protocol":
|
|
294
|
+
is_protocol = True
|
|
295
|
+
break
|
|
296
|
+
except Exception:
|
|
297
|
+
is_protocol = False
|
|
298
|
+
|
|
299
|
+
# NamedTuple detection
|
|
300
|
+
is_namedtuple = hasattr(cls, "_fields") and isinstance(getattr(cls, "_fields", None), (list, tuple))
|
|
301
|
+
if is_namedtuple and not fields:
|
|
302
|
+
try:
|
|
303
|
+
ann = get_type_hints(cls, include_extras=True) if hasattr(typing, "get_origin") else getattr(cls, "__annotations__", {})
|
|
304
|
+
for fname in getattr(cls, "_fields", []):
|
|
305
|
+
ftype = ann.get(fname, None)
|
|
306
|
+
fields.append(IRParam(name=str(fname), kind="FIELD", annotation=_stringify_annotation(ftype), default=False))
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
|
|
310
|
+
# Dataclass detection
|
|
311
|
+
is_dataclass = False
|
|
312
|
+
try:
|
|
313
|
+
is_dataclass = _dataclasses.is_dataclass(cls)
|
|
314
|
+
except Exception:
|
|
315
|
+
is_dataclass = False
|
|
316
|
+
if is_dataclass and not fields:
|
|
317
|
+
try:
|
|
318
|
+
for f in _dataclasses.fields(cls): # type: ignore[attr-defined]
|
|
319
|
+
defaulted = not (f.default is _dataclasses.MISSING and f.default_factory is _dataclasses.MISSING) # type: ignore[attr-defined]
|
|
320
|
+
fields.append(IRParam(name=f.name, kind="FIELD", annotation=_stringify_annotation(f.type), default=defaulted))
|
|
321
|
+
except Exception:
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
# Pydantic detection
|
|
325
|
+
is_pydantic = False
|
|
326
|
+
try:
|
|
327
|
+
import pydantic
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
base = pydantic.BaseModel # type: ignore[attr-defined]
|
|
331
|
+
except Exception:
|
|
332
|
+
base = None
|
|
333
|
+
if base is not None:
|
|
334
|
+
try:
|
|
335
|
+
is_pydantic = issubclass(cls, base)
|
|
336
|
+
except Exception:
|
|
337
|
+
is_pydantic = False
|
|
338
|
+
except Exception:
|
|
339
|
+
is_pydantic = False
|
|
340
|
+
if is_pydantic and not fields:
|
|
341
|
+
try:
|
|
342
|
+
# v2
|
|
343
|
+
model_fields = getattr(cls, "model_fields", None)
|
|
344
|
+
if isinstance(model_fields, dict):
|
|
345
|
+
for fname, finfo in model_fields.items():
|
|
346
|
+
ann = getattr(finfo, "annotation", None)
|
|
347
|
+
required = getattr(finfo, "is_required", False)
|
|
348
|
+
fields.append(IRParam(name=str(fname), kind="FIELD", annotation=_stringify_annotation(ann), default=(not required)))
|
|
349
|
+
else:
|
|
350
|
+
# v1
|
|
351
|
+
__fields__ = getattr(cls, "__fields__", None)
|
|
352
|
+
if isinstance(__fields__, dict):
|
|
353
|
+
for fname, finfo in __fields__.items():
|
|
354
|
+
ann = getattr(finfo, "type_", None)
|
|
355
|
+
required = getattr(finfo, "required", False)
|
|
356
|
+
fields.append(IRParam(name=str(fname), kind="FIELD", annotation=_stringify_annotation(ann), default=(not required)))
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
|
|
360
|
+
return IRClass(
|
|
361
|
+
name=name,
|
|
362
|
+
qualname=f"{module_name}.{name}",
|
|
363
|
+
docstring=inspect.getdoc(cls),
|
|
364
|
+
bases=bases,
|
|
365
|
+
methods=methods,
|
|
366
|
+
typed_dict=typed_dict,
|
|
367
|
+
total=total,
|
|
368
|
+
fields=fields,
|
|
369
|
+
is_protocol=is_protocol,
|
|
370
|
+
is_namedtuple=is_namedtuple,
|
|
371
|
+
is_dataclass=is_dataclass,
|
|
372
|
+
is_pydantic=is_pydantic,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _collect_metadata(module_name: str, ir_version: str) -> Dict[str, Any]:
|
|
377
|
+
py_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
378
|
+
plat = platform.platform()
|
|
379
|
+
|
|
380
|
+
pkg_root = module_name.split(".")[0]
|
|
381
|
+
version: Optional[str]
|
|
382
|
+
try:
|
|
383
|
+
version = importlib_metadata.version(pkg_root)
|
|
384
|
+
except Exception:
|
|
385
|
+
try:
|
|
386
|
+
mod = importlib.import_module(pkg_root)
|
|
387
|
+
version = getattr(mod, "__version__", None)
|
|
388
|
+
except Exception:
|
|
389
|
+
version = None
|
|
390
|
+
|
|
391
|
+
cache_key = f"{module_name}@{version or 'unknown'}|py{py_version}|ir{ir_version}"
|
|
392
|
+
return {
|
|
393
|
+
"python_version": py_version,
|
|
394
|
+
"platform": plat,
|
|
395
|
+
"package": pkg_root,
|
|
396
|
+
"package_version": version,
|
|
397
|
+
"cache_key": cache_key,
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def extract_module_ir(
|
|
402
|
+
module_name: str,
|
|
403
|
+
*,
|
|
404
|
+
ir_version: str = "0.1.0",
|
|
405
|
+
include_private: bool = False,
|
|
406
|
+
) -> Dict[str, Any]:
|
|
407
|
+
"""
|
|
408
|
+
Extract a minimal IR for a Python module: top-level callables with signature info.
|
|
409
|
+
"""
|
|
410
|
+
module = importlib.import_module(module_name)
|
|
411
|
+
|
|
412
|
+
functions: List[IRFunction] = []
|
|
413
|
+
classes: List[IRClass] = []
|
|
414
|
+
warnings: List[str] = []
|
|
415
|
+
|
|
416
|
+
# Extract constants and type aliases
|
|
417
|
+
constants = _extract_constants(module, module_name, include_private)
|
|
418
|
+
type_aliases = _extract_type_aliases(module, module_name, include_private)
|
|
419
|
+
|
|
420
|
+
for name in dir(module):
|
|
421
|
+
try:
|
|
422
|
+
value = getattr(module, name)
|
|
423
|
+
except Exception:
|
|
424
|
+
continue
|
|
425
|
+
if not include_private and name.startswith("_"):
|
|
426
|
+
continue
|
|
427
|
+
# Include plain functions and builtins (e.g., math.sqrt)
|
|
428
|
+
if inspect.isfunction(value) or inspect.isbuiltin(value):
|
|
429
|
+
fn = _extract_function(value, f"{module_name}.{name}")
|
|
430
|
+
if fn is not None:
|
|
431
|
+
functions.append(fn)
|
|
432
|
+
# Include classes defined in this module
|
|
433
|
+
if inspect.isclass(value) and getattr(value, "__module__", None) == module.__name__:
|
|
434
|
+
cls_ir = _extract_class(value, module_name, include_private)
|
|
435
|
+
if cls_ir is not None:
|
|
436
|
+
classes.append(cls_ir)
|
|
437
|
+
|
|
438
|
+
ir = IRModule(
|
|
439
|
+
ir_version=ir_version,
|
|
440
|
+
module=module_name,
|
|
441
|
+
functions=functions,
|
|
442
|
+
classes=classes,
|
|
443
|
+
constants=constants,
|
|
444
|
+
type_aliases=type_aliases,
|
|
445
|
+
metadata=_collect_metadata(module_name, ir_version),
|
|
446
|
+
warnings=warnings,
|
|
447
|
+
)
|
|
448
|
+
# Return as plain dicts ready for JSON emitting
|
|
449
|
+
return asdict(ir)
|
|
450
|
+
|
|
451
|
+
def emit_ir_json(
|
|
452
|
+
module_name: str,
|
|
453
|
+
*,
|
|
454
|
+
ir_version: str = "0.1.0",
|
|
455
|
+
include_private: bool = False,
|
|
456
|
+
pretty: bool = True,
|
|
457
|
+
) -> str:
|
|
458
|
+
return json.dumps(
|
|
459
|
+
extract_module_ir(module_name, ir_version=ir_version, include_private=include_private),
|
|
460
|
+
ensure_ascii=False,
|
|
461
|
+
indent=2 if pretty else None,
|
|
462
|
+
)
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optimized IR Extraction for tywrap
|
|
3
|
+
High-performance Python module introspection with caching and parallel processing
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import threading
|
|
11
|
+
import concurrent.futures
|
|
12
|
+
from functools import lru_cache, wraps
|
|
13
|
+
from typing import Dict, List, Any, Optional, Set, Callable
|
|
14
|
+
import weakref
|
|
15
|
+
import gc
|
|
16
|
+
from dataclasses import dataclass, asdict
|
|
17
|
+
|
|
18
|
+
# Reuse existing IR classes from ir.py
|
|
19
|
+
from .ir import (
|
|
20
|
+
IRParam, IRFunction, IRClass, IRConstant, IRTypeAlias, IRModule,
|
|
21
|
+
_stringify_annotation, _param_kind_to_str, _extract_function, _extract_class,
|
|
22
|
+
_extract_constants, _extract_type_aliases, _collect_metadata
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PerformanceTimer:
|
|
27
|
+
"""High-precision performance timer for profiling"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, name: str = ""):
|
|
30
|
+
self.name = name
|
|
31
|
+
self.start_time = 0.0
|
|
32
|
+
self.end_time = 0.0
|
|
33
|
+
|
|
34
|
+
def __enter__(self):
|
|
35
|
+
self.start_time = time.perf_counter()
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
39
|
+
self.end_time = time.perf_counter()
|
|
40
|
+
duration = self.duration
|
|
41
|
+
print(f"ā” {self.name}: {duration:.2f}ms", file=sys.stderr)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def duration(self) -> float:
|
|
45
|
+
"""Duration in milliseconds"""
|
|
46
|
+
return (self.end_time - self.start_time) * 1000
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ExtractionStats:
|
|
51
|
+
"""Statistics about IR extraction performance"""
|
|
52
|
+
total_time: float
|
|
53
|
+
functions_extracted: int
|
|
54
|
+
classes_extracted: int
|
|
55
|
+
constants_extracted: int
|
|
56
|
+
type_aliases_extracted: int
|
|
57
|
+
cache_hits: int
|
|
58
|
+
cache_misses: int
|
|
59
|
+
memory_peak: int # bytes
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class IRCache:
|
|
63
|
+
"""Intelligent caching for IR extraction results"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, max_size: int = 1000):
|
|
66
|
+
self.max_size = max_size
|
|
67
|
+
self._cache: Dict[str, Any] = {}
|
|
68
|
+
self._access_times: Dict[str, float] = {}
|
|
69
|
+
self._lock = threading.RLock()
|
|
70
|
+
self.hits = 0
|
|
71
|
+
self.misses = 0
|
|
72
|
+
|
|
73
|
+
def _make_key(self, *args) -> str:
|
|
74
|
+
"""Create cache key from arguments"""
|
|
75
|
+
return "|".join(str(arg) for arg in args)
|
|
76
|
+
|
|
77
|
+
def get(self, key: str) -> Optional[Any]:
|
|
78
|
+
"""Get item from cache"""
|
|
79
|
+
with self._lock:
|
|
80
|
+
if key in self._cache:
|
|
81
|
+
self._access_times[key] = time.time()
|
|
82
|
+
self.hits += 1
|
|
83
|
+
return self._cache[key]
|
|
84
|
+
|
|
85
|
+
self.misses += 1
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def set(self, key: str, value: Any) -> None:
|
|
89
|
+
"""Set item in cache with LRU eviction"""
|
|
90
|
+
with self._lock:
|
|
91
|
+
# Evict oldest items if at capacity
|
|
92
|
+
if len(self._cache) >= self.max_size and key not in self._cache:
|
|
93
|
+
oldest_key = min(self._access_times.keys(), key=lambda k: self._access_times[k])
|
|
94
|
+
del self._cache[oldest_key]
|
|
95
|
+
del self._access_times[oldest_key]
|
|
96
|
+
|
|
97
|
+
self._cache[key] = value
|
|
98
|
+
self._access_times[key] = time.time()
|
|
99
|
+
|
|
100
|
+
def cached_function(self, func: Callable) -> Callable:
|
|
101
|
+
"""Decorator for caching function results"""
|
|
102
|
+
@wraps(func)
|
|
103
|
+
def wrapper(*args, **kwargs):
|
|
104
|
+
# Create cache key from function name and arguments
|
|
105
|
+
key_parts = [func.__name__] + list(args) + [f"{k}={v}" for k, v in sorted(kwargs.items())]
|
|
106
|
+
key = self._make_key(*key_parts)
|
|
107
|
+
|
|
108
|
+
result = self.get(key)
|
|
109
|
+
if result is not None:
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
result = func(*args, **kwargs)
|
|
113
|
+
self.set(key, result)
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
return wrapper
|
|
117
|
+
|
|
118
|
+
def clear(self) -> None:
|
|
119
|
+
"""Clear all cache entries"""
|
|
120
|
+
with self._lock:
|
|
121
|
+
self._cache.clear()
|
|
122
|
+
self._access_times.clear()
|
|
123
|
+
self.hits = 0
|
|
124
|
+
self.misses = 0
|
|
125
|
+
|
|
126
|
+
def stats(self) -> Dict[str, Any]:
|
|
127
|
+
"""Get cache statistics"""
|
|
128
|
+
with self._lock:
|
|
129
|
+
total_requests = self.hits + self.misses
|
|
130
|
+
hit_rate = self.hits / max(1, total_requests)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"size": len(self._cache),
|
|
134
|
+
"max_size": self.max_size,
|
|
135
|
+
"hits": self.hits,
|
|
136
|
+
"misses": self.misses,
|
|
137
|
+
"hit_rate": hit_rate,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Global cache instance
|
|
142
|
+
_global_cache = IRCache()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class OptimizedIRExtractor:
|
|
146
|
+
"""High-performance IR extraction with optimization techniques"""
|
|
147
|
+
|
|
148
|
+
def __init__(self,
|
|
149
|
+
enable_caching: bool = True,
|
|
150
|
+
enable_parallel: bool = True,
|
|
151
|
+
max_workers: int = 4):
|
|
152
|
+
self.enable_caching = enable_caching
|
|
153
|
+
self.enable_parallel = enable_parallel
|
|
154
|
+
self.max_workers = max_workers
|
|
155
|
+
self._cache = _global_cache if enable_caching else None
|
|
156
|
+
self._stats = ExtractionStats(0, 0, 0, 0, 0, 0, 0, 0)
|
|
157
|
+
|
|
158
|
+
def extract_module_ir_optimized(self,
|
|
159
|
+
module_name: str,
|
|
160
|
+
*,
|
|
161
|
+
ir_version: str = "0.1.0",
|
|
162
|
+
include_private: bool = False) -> Dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
Extract IR with performance optimizations
|
|
165
|
+
"""
|
|
166
|
+
with PerformanceTimer(f"IR extraction for {module_name}"):
|
|
167
|
+
return self._extract_with_optimizations(module_name, ir_version, include_private)
|
|
168
|
+
|
|
169
|
+
def _extract_with_optimizations(self,
|
|
170
|
+
module_name: str,
|
|
171
|
+
ir_version: str,
|
|
172
|
+
include_private: bool) -> Dict[str, Any]:
|
|
173
|
+
"""Internal optimized extraction"""
|
|
174
|
+
|
|
175
|
+
# Try cache first
|
|
176
|
+
if self._cache:
|
|
177
|
+
cache_key = self._cache._make_key(module_name, ir_version, include_private)
|
|
178
|
+
cached_result = self._cache.get(cache_key)
|
|
179
|
+
if cached_result is not None:
|
|
180
|
+
print(f"šÆ Cache HIT for {module_name}", file=sys.stderr)
|
|
181
|
+
return cached_result
|
|
182
|
+
|
|
183
|
+
# Import module with error handling
|
|
184
|
+
try:
|
|
185
|
+
import importlib
|
|
186
|
+
module = importlib.import_module(module_name)
|
|
187
|
+
except ImportError as e:
|
|
188
|
+
raise ImportError(f"Failed to import module {module_name}: {e}")
|
|
189
|
+
|
|
190
|
+
# Extract components in parallel if enabled
|
|
191
|
+
if self.enable_parallel and self.max_workers > 1:
|
|
192
|
+
result = self._extract_parallel(module, module_name, ir_version, include_private)
|
|
193
|
+
else:
|
|
194
|
+
result = self._extract_sequential(module, module_name, ir_version, include_private)
|
|
195
|
+
|
|
196
|
+
# Cache result
|
|
197
|
+
if self._cache:
|
|
198
|
+
self._cache.set(cache_key, result)
|
|
199
|
+
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
def _extract_parallel(self,
|
|
203
|
+
module: Any,
|
|
204
|
+
module_name: str,
|
|
205
|
+
ir_version: str,
|
|
206
|
+
include_private: bool) -> Dict[str, Any]:
|
|
207
|
+
"""Extract IR components in parallel"""
|
|
208
|
+
|
|
209
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
210
|
+
# Submit all extraction tasks
|
|
211
|
+
futures = {
|
|
212
|
+
'functions': executor.submit(self._extract_functions_optimized, module, module_name, include_private),
|
|
213
|
+
'classes': executor.submit(self._extract_classes_optimized, module, module_name, include_private),
|
|
214
|
+
'constants': executor.submit(self._extract_constants_optimized, module, module_name, include_private),
|
|
215
|
+
'type_aliases': executor.submit(self._extract_type_aliases_optimized, module, module_name, include_private),
|
|
216
|
+
'metadata': executor.submit(_collect_metadata, module_name, ir_version),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# Collect results
|
|
220
|
+
results = {}
|
|
221
|
+
for key, future in futures.items():
|
|
222
|
+
try:
|
|
223
|
+
results[key] = future.result(timeout=30) # 30 second timeout
|
|
224
|
+
except concurrent.futures.TimeoutError:
|
|
225
|
+
print(f"ā ļø Timeout extracting {key} from {module_name}", file=sys.stderr)
|
|
226
|
+
results[key] = [] if key != 'metadata' else {}
|
|
227
|
+
except Exception as e:
|
|
228
|
+
print(f"ā Error extracting {key} from {module_name}: {e}", file=sys.stderr)
|
|
229
|
+
results[key] = [] if key != 'metadata' else {}
|
|
230
|
+
|
|
231
|
+
# Build IR module
|
|
232
|
+
ir_module = IRModule(
|
|
233
|
+
ir_version=ir_version,
|
|
234
|
+
module=module_name,
|
|
235
|
+
functions=results.get('functions', []),
|
|
236
|
+
classes=results.get('classes', []),
|
|
237
|
+
constants=results.get('constants', []),
|
|
238
|
+
type_aliases=results.get('type_aliases', []),
|
|
239
|
+
metadata=results.get('metadata', {}),
|
|
240
|
+
warnings=[] # TODO: Collect warnings from parallel execution
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Update statistics
|
|
244
|
+
self._stats.functions_extracted += len(ir_module.functions)
|
|
245
|
+
self._stats.classes_extracted += len(ir_module.classes)
|
|
246
|
+
self._stats.constants_extracted += len(ir_module.constants)
|
|
247
|
+
self._stats.type_aliases_extracted += len(ir_module.type_aliases)
|
|
248
|
+
|
|
249
|
+
return asdict(ir_module)
|
|
250
|
+
|
|
251
|
+
def _extract_sequential(self,
|
|
252
|
+
module: Any,
|
|
253
|
+
module_name: str,
|
|
254
|
+
ir_version: str,
|
|
255
|
+
include_private: bool) -> Dict[str, Any]:
|
|
256
|
+
"""Extract IR components sequentially (fallback)"""
|
|
257
|
+
|
|
258
|
+
with PerformanceTimer("Sequential extraction"):
|
|
259
|
+
functions = self._extract_functions_optimized(module, module_name, include_private)
|
|
260
|
+
classes = self._extract_classes_optimized(module, module_name, include_private)
|
|
261
|
+
constants = self._extract_constants_optimized(module, module_name, include_private)
|
|
262
|
+
type_aliases = self._extract_type_aliases_optimized(module, module_name, include_private)
|
|
263
|
+
metadata = _collect_metadata(module_name, ir_version)
|
|
264
|
+
|
|
265
|
+
ir_module = IRModule(
|
|
266
|
+
ir_version=ir_version,
|
|
267
|
+
module=module_name,
|
|
268
|
+
functions=functions,
|
|
269
|
+
classes=classes,
|
|
270
|
+
constants=constants,
|
|
271
|
+
type_aliases=type_aliases,
|
|
272
|
+
metadata=metadata,
|
|
273
|
+
warnings=[]
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return asdict(ir_module)
|
|
277
|
+
|
|
278
|
+
@lru_cache(maxsize=128)
|
|
279
|
+
def _get_module_members(self, module_name: str) -> List[str]:
|
|
280
|
+
"""Cached module member listing"""
|
|
281
|
+
import importlib
|
|
282
|
+
module = importlib.import_module(module_name)
|
|
283
|
+
return dir(module)
|
|
284
|
+
|
|
285
|
+
def _extract_functions_optimized(self,
|
|
286
|
+
module: Any,
|
|
287
|
+
module_name: str,
|
|
288
|
+
include_private: bool) -> List[IRFunction]:
|
|
289
|
+
"""Optimized function extraction"""
|
|
290
|
+
import inspect
|
|
291
|
+
|
|
292
|
+
functions: List[IRFunction] = []
|
|
293
|
+
members = self._get_module_members(module_name)
|
|
294
|
+
|
|
295
|
+
# Pre-filter members to reduce attribute access
|
|
296
|
+
function_names = []
|
|
297
|
+
for name in members:
|
|
298
|
+
if not include_private and name.startswith("_"):
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
value = getattr(module, name, None)
|
|
303
|
+
if inspect.isfunction(value) or inspect.isbuiltin(value):
|
|
304
|
+
function_names.append(name)
|
|
305
|
+
except (AttributeError, ImportError):
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
# Extract functions with caching
|
|
309
|
+
for name in function_names:
|
|
310
|
+
try:
|
|
311
|
+
value = getattr(module, name)
|
|
312
|
+
if self._cache:
|
|
313
|
+
cached_func = self._cache.cached_function(_extract_function)
|
|
314
|
+
func_ir = cached_func(value, f"{module_name}.{name}")
|
|
315
|
+
else:
|
|
316
|
+
func_ir = _extract_function(value, f"{module_name}.{name}")
|
|
317
|
+
|
|
318
|
+
if func_ir is not None:
|
|
319
|
+
functions.append(func_ir)
|
|
320
|
+
except Exception as e:
|
|
321
|
+
print(f"ā ļø Error extracting function {name}: {e}", file=sys.stderr)
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
return functions
|
|
325
|
+
|
|
326
|
+
def _extract_classes_optimized(self,
|
|
327
|
+
module: Any,
|
|
328
|
+
module_name: str,
|
|
329
|
+
include_private: bool) -> List[IRClass]:
|
|
330
|
+
"""Optimized class extraction"""
|
|
331
|
+
import inspect
|
|
332
|
+
|
|
333
|
+
classes: List[IRClass] = []
|
|
334
|
+
members = self._get_module_members(module_name)
|
|
335
|
+
|
|
336
|
+
# Pre-filter for classes defined in this module
|
|
337
|
+
class_names = []
|
|
338
|
+
for name in members:
|
|
339
|
+
if not include_private and name.startswith("_"):
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
value = getattr(module, name, None)
|
|
344
|
+
if (inspect.isclass(value) and
|
|
345
|
+
getattr(value, "__module__", None) == module.__name__):
|
|
346
|
+
class_names.append(name)
|
|
347
|
+
except (AttributeError, ImportError):
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
# Extract classes with caching
|
|
351
|
+
for name in class_names:
|
|
352
|
+
try:
|
|
353
|
+
value = getattr(module, name)
|
|
354
|
+
if self._cache:
|
|
355
|
+
cached_class = self._cache.cached_function(_extract_class)
|
|
356
|
+
class_ir = cached_class(value, module_name, include_private)
|
|
357
|
+
else:
|
|
358
|
+
class_ir = _extract_class(value, module_name, include_private)
|
|
359
|
+
|
|
360
|
+
if class_ir is not None:
|
|
361
|
+
classes.append(class_ir)
|
|
362
|
+
except Exception as e:
|
|
363
|
+
print(f"ā ļø Error extracting class {name}: {e}", file=sys.stderr)
|
|
364
|
+
continue
|
|
365
|
+
|
|
366
|
+
return classes
|
|
367
|
+
|
|
368
|
+
def _extract_constants_optimized(self,
|
|
369
|
+
module: Any,
|
|
370
|
+
module_name: str,
|
|
371
|
+
include_private: bool) -> List[IRConstant]:
|
|
372
|
+
"""Optimized constant extraction with caching"""
|
|
373
|
+
if self._cache:
|
|
374
|
+
cached_constants = self._cache.cached_function(_extract_constants)
|
|
375
|
+
return cached_constants(module, module_name, include_private)
|
|
376
|
+
else:
|
|
377
|
+
return _extract_constants(module, module_name, include_private)
|
|
378
|
+
|
|
379
|
+
def _extract_type_aliases_optimized(self,
|
|
380
|
+
module: Any,
|
|
381
|
+
module_name: str,
|
|
382
|
+
include_private: bool) -> List[IRTypeAlias]:
|
|
383
|
+
"""Optimized type alias extraction with caching"""
|
|
384
|
+
if self._cache:
|
|
385
|
+
cached_aliases = self._cache.cached_function(_extract_type_aliases)
|
|
386
|
+
return cached_aliases(module, module_name, include_private)
|
|
387
|
+
else:
|
|
388
|
+
return _extract_type_aliases(module, module_name, include_private)
|
|
389
|
+
|
|
390
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
391
|
+
"""Get extraction statistics"""
|
|
392
|
+
stats_dict = asdict(self._stats)
|
|
393
|
+
if self._cache:
|
|
394
|
+
stats_dict["cache"] = self._cache.stats()
|
|
395
|
+
return stats_dict
|
|
396
|
+
|
|
397
|
+
def clear_cache(self) -> None:
|
|
398
|
+
"""Clear extraction cache"""
|
|
399
|
+
if self._cache:
|
|
400
|
+
self._cache.clear()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# Global optimized extractor instance
|
|
404
|
+
_global_extractor = OptimizedIRExtractor()
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def extract_module_ir_optimized(module_name: str,
|
|
408
|
+
*,
|
|
409
|
+
ir_version: str = "0.1.0",
|
|
410
|
+
include_private: bool = False,
|
|
411
|
+
enable_caching: bool = True,
|
|
412
|
+
enable_parallel: bool = True) -> Dict[str, Any]:
|
|
413
|
+
"""
|
|
414
|
+
Optimized IR extraction with caching and parallel processing
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
module_name: Name of the module to extract
|
|
418
|
+
ir_version: IR format version
|
|
419
|
+
include_private: Include private members
|
|
420
|
+
enable_caching: Enable result caching
|
|
421
|
+
enable_parallel: Enable parallel extraction
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Dictionary containing the IR module data
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
# Configure extractor if needed
|
|
428
|
+
if not enable_caching and _global_extractor.enable_caching:
|
|
429
|
+
_global_extractor._cache = None
|
|
430
|
+
_global_extractor.enable_caching = False
|
|
431
|
+
|
|
432
|
+
if not enable_parallel and _global_extractor.enable_parallel:
|
|
433
|
+
_global_extractor.enable_parallel = False
|
|
434
|
+
|
|
435
|
+
return _global_extractor.extract_module_ir_optimized(
|
|
436
|
+
module_name,
|
|
437
|
+
ir_version=ir_version,
|
|
438
|
+
include_private=include_private
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def benchmark_ir_extraction(module_names: List[str],
|
|
443
|
+
iterations: int = 3,
|
|
444
|
+
enable_optimizations: bool = True) -> Dict[str, Any]:
|
|
445
|
+
"""
|
|
446
|
+
Benchmark IR extraction performance
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
module_names: List of module names to benchmark
|
|
450
|
+
iterations: Number of iterations per module
|
|
451
|
+
enable_optimizations: Whether to enable optimizations
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Benchmark results
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
results = {
|
|
458
|
+
"modules": {},
|
|
459
|
+
"summary": {
|
|
460
|
+
"total_modules": len(module_names),
|
|
461
|
+
"iterations": iterations,
|
|
462
|
+
"optimizations_enabled": enable_optimizations,
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
extractor = OptimizedIRExtractor(
|
|
467
|
+
enable_caching=enable_optimizations,
|
|
468
|
+
enable_parallel=enable_optimizations
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
for module_name in module_names:
|
|
472
|
+
module_times = []
|
|
473
|
+
module_results = []
|
|
474
|
+
|
|
475
|
+
for i in range(iterations):
|
|
476
|
+
if i == 0 or not enable_optimizations:
|
|
477
|
+
# Clear cache for first iteration or when optimizations disabled
|
|
478
|
+
extractor.clear_cache()
|
|
479
|
+
gc.collect() # Force garbage collection
|
|
480
|
+
|
|
481
|
+
start_time = time.perf_counter()
|
|
482
|
+
try:
|
|
483
|
+
result = extractor.extract_module_ir_optimized(module_name)
|
|
484
|
+
module_results.append(result)
|
|
485
|
+
end_time = time.perf_counter()
|
|
486
|
+
duration = (end_time - start_time) * 1000 # milliseconds
|
|
487
|
+
module_times.append(duration)
|
|
488
|
+
|
|
489
|
+
print(f"š {module_name} iteration {i+1}: {duration:.2f}ms", file=sys.stderr)
|
|
490
|
+
|
|
491
|
+
except Exception as e:
|
|
492
|
+
print(f"ā Error benchmarking {module_name}: {e}", file=sys.stderr)
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
if module_times:
|
|
496
|
+
results["modules"][module_name] = {
|
|
497
|
+
"times": module_times,
|
|
498
|
+
"min_time": min(module_times),
|
|
499
|
+
"max_time": max(module_times),
|
|
500
|
+
"avg_time": sum(module_times) / len(module_times),
|
|
501
|
+
"speedup": module_times[0] / min(module_times) if len(module_times) > 1 else 1.0,
|
|
502
|
+
"functions_count": len(module_results[-1]["functions"]) if module_results else 0,
|
|
503
|
+
"classes_count": len(module_results[-1]["classes"]) if module_results else 0,
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
# Overall statistics
|
|
507
|
+
all_times = [time for module_data in results["modules"].values() for time in module_data["times"]]
|
|
508
|
+
if all_times:
|
|
509
|
+
results["summary"].update({
|
|
510
|
+
"total_extractions": len(all_times),
|
|
511
|
+
"min_time": min(all_times),
|
|
512
|
+
"max_time": max(all_times),
|
|
513
|
+
"avg_time": sum(all_times) / len(all_times),
|
|
514
|
+
"total_time": sum(all_times),
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
# Cache statistics
|
|
518
|
+
cache_stats = extractor.get_stats()
|
|
519
|
+
results["cache_stats"] = cache_stats
|
|
520
|
+
|
|
521
|
+
return results
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
if __name__ == "__main__":
|
|
525
|
+
import json
|
|
526
|
+
|
|
527
|
+
# Example usage and benchmarking
|
|
528
|
+
test_modules = ["math", "json", "os", "sys", "time"]
|
|
529
|
+
|
|
530
|
+
print("š Benchmarking optimized IR extraction", file=sys.stderr)
|
|
531
|
+
|
|
532
|
+
# Benchmark with optimizations
|
|
533
|
+
optimized_results = benchmark_ir_extraction(
|
|
534
|
+
test_modules,
|
|
535
|
+
iterations=3,
|
|
536
|
+
enable_optimizations=True
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
print("\nš Benchmark Results (Optimized):", file=sys.stderr)
|
|
540
|
+
for module, data in optimized_results["modules"].items():
|
|
541
|
+
print(f" {module}: {data['avg_time']:.2f}ms avg, {data['speedup']:.2f}x speedup", file=sys.stderr)
|
|
542
|
+
|
|
543
|
+
# Output results as JSON for analysis
|
|
544
|
+
print(json.dumps(optimized_results, indent=2))
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tywrap-ir
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Python IR extractor for tywrap: emits versioned JSON IR for Python modules
|
|
5
|
+
Author: tywrap contributors
|
|
6
|
+
Maintainer: tywrap contributors
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/bbopen/tywrap
|
|
9
|
+
Project-URL: Repository, https://github.com/bbopen/tywrap
|
|
10
|
+
Project-URL: Documentation, https://github.com/bbopen/tywrap#readme
|
|
11
|
+
Project-URL: Issues, https://github.com/bbopen/tywrap/issues
|
|
12
|
+
Keywords: tywrap,typescript,python,bridge,code-generation,ast,ir
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# tywrap-ir
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/tywrap-ir/)
|
|
30
|
+
[](https://pypi.org/project/tywrap-ir/)
|
|
31
|
+
[](https://opensource.org/licenses/MIT)
|
|
32
|
+
|
|
33
|
+
Python IR extractor for [tywrap](https://github.com/bbopen/tywrap). Emits versioned JSON IR for Python modules using inspect/typing/importlib.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install tywrap-ir
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Extract IR for a module
|
|
45
|
+
python -m tywrap_ir --module math
|
|
46
|
+
|
|
47
|
+
# Or using the CLI
|
|
48
|
+
tywrap-ir --module math
|
|
49
|
+
|
|
50
|
+
# Output to file
|
|
51
|
+
tywrap-ir --module pandas --output pandas_ir.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## What is this?
|
|
55
|
+
|
|
56
|
+
This package is the Python component of tywrap, a TypeScript wrapper generator for Python libraries. It analyzes Python modules and extracts type information into a JSON intermediate representation (IR) that tywrap uses to generate TypeScript bindings.
|
|
57
|
+
|
|
58
|
+
You typically don't need to use this package directly - the `tywrap` npm package invokes it automatically during code generation.
|
|
59
|
+
|
|
60
|
+
## Requirements
|
|
61
|
+
|
|
62
|
+
- Python 3.10+
|
|
63
|
+
|
|
64
|
+
## Related
|
|
65
|
+
|
|
66
|
+
- [tywrap](https://www.npmjs.com/package/tywrap) - The main TypeScript package
|
|
67
|
+
- [GitHub](https://github.com/bbopen/tywrap) - Source code and issues
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
tywrap_ir/__init__.py
|
|
4
|
+
tywrap_ir/__main__.py
|
|
5
|
+
tywrap_ir/ir.py
|
|
6
|
+
tywrap_ir/optimized_ir.py
|
|
7
|
+
tywrap_ir.egg-info/PKG-INFO
|
|
8
|
+
tywrap_ir.egg-info/SOURCES.txt
|
|
9
|
+
tywrap_ir.egg-info/dependency_links.txt
|
|
10
|
+
tywrap_ir.egg-info/entry_points.txt
|
|
11
|
+
tywrap_ir.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tywrap_ir
|