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.
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/tywrap-ir.svg)](https://pypi.org/project/tywrap-ir/)
30
+ [![Python versions](https://img.shields.io/pypi/pyversions/tywrap-ir.svg)](https://pypi.org/project/tywrap-ir/)
31
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/tywrap-ir.svg)](https://pypi.org/project/tywrap-ir/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/tywrap-ir.svg)](https://pypi.org/project/tywrap-ir/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ __all__ = [
2
+ "extract_module_ir",
3
+ "IR_VERSION",
4
+ "__version__",
5
+ ]
6
+
7
+ __version__ = "0.1.2"
8
+ IR_VERSION = "0.1.0"
9
+
10
+ from .ir import extract_module_ir # noqa: E402
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/tywrap-ir.svg)](https://pypi.org/project/tywrap-ir/)
30
+ [![Python versions](https://img.shields.io/pypi/pyversions/tywrap-ir.svg)](https://pypi.org/project/tywrap-ir/)
31
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,2 @@
1
+ [console_scripts]
2
+ tywrap-ir = tywrap_ir.__main__:main
@@ -0,0 +1 @@
1
+ tywrap_ir