PK!3CPPpy_ts_interfaces/__init__.pyfrom .parser import Interface as Interface, Parser as Parser import collections PK!3&7 7 py_ts_interfaces/cli.pyfrom collections import deque from py_ts_interfaces import Interface, Parser from typing import Iterable, List, Set import argparse import os import warnings def main() -> None: args = get_args_namespace() if os.path.isdir(args.outpath): raise Exception(f"{args.outpath} is a directory! Aborting.") interface_parser = Parser(f"{Interface.__module__}.{Interface.__name__}") for code in read_code_from_files(get_paths_to_py_files(args.paths)): interface_parser.parse(code) result = interface_parser.flush() if not result: warnings.warn("Did not have anything to write to the file!", UserWarning) if not args.should_append or not os.path.isfile(args.outpath): with open(args.outpath, "w") as f: f.write( "// Generated using py-ts-interfaces. " "See https://github.com/cs-cordero/py-ts-interfaces\n\n" ) f.write(result) print(f"Created {args.outpath}!") else: with open(args.outpath, "a") as f: f.write(result) print(f"Appended to {args.outpath}!") def get_args_namespace() -> argparse.Namespace: argparser = argparse.ArgumentParser( description="Generates TypeScript Interfaces from subclasses of" " py_ts_interfaces.Interface." ) argparser.add_argument("paths", action="store", nargs="+") argparser.add_argument( "-o, --outpath", action="store", default="interface.ts", dest="outpath" ) argparser.add_argument("-a, --append", action="store_true", dest="should_append") return argparser.parse_args() def get_paths_to_py_files(raw_paths: List[str]) -> Set[str]: paths: Set[str] = set() queue = deque(raw_paths) while queue: path = queue.popleft() if os.path.isfile(path): if path.endswith(".py"): paths.add(path) continue if os.path.isdir(path): queue.extend( [os.path.join(path, next_path) for next_path in os.listdir(path)] ) continue warnings.warn(f"Skipping {path}!", UserWarning) return paths def read_code_from_files(paths: Iterable[str]) -> Iterable[str]: for path in paths: with open(path, "r") as f: yield f.read() if __name__ == "__main__": main() PK!py_ts_interfaces/parser.pyfrom collections import deque from typing import Dict, List, NamedTuple, Optional import astroid import warnings class Interface: pass TYPE_MAP: Dict[str, str] = { "bool": "boolean", "str": "string", "int": "number", "float": "number", "complex": "number", "Any": "any", "List": "Array", "Tuple": "[any]", "Union": "any", } SUBSCRIPT_FORMAT_MAP: Dict[str, str] = { "List": "Array<%s>", "Optional": "%s | null", "Tuple": "[%s]", "Union": "%s", } InterfaceAttributes = Dict[str, str] PreparedInterfaces = Dict[str, InterfaceAttributes] class Parser: def __init__(self, interface_qualname: str) -> None: self.interface_qualname = interface_qualname self.prepared: PreparedInterfaces = {} def parse(self, code: str) -> None: queue = deque([astroid.parse(code)]) while queue: current = queue.popleft() children = current.get_children() if not isinstance(current, astroid.ClassDef): queue.extend(children) continue if not current.is_subtype_of(self.interface_qualname): queue.extend(children) continue if not has_dataclass_decorator(current.decorators): warnings.warn( "Non-dataclasses are not supported, see documentation.", UserWarning ) continue if current.name in self.prepared: warnings.warn( f"Found duplicate interface with name {current.name}." "All interfaces after the first will be ignored", UserWarning, ) continue self.prepared[current.name] = get_types_from_classdef(current) def flush(self) -> str: serialized: List[str] = [] for interface, attributes in self.prepared.items(): s = f"interface {interface} {{\n" for attribute_name, attribute_type in attributes.items(): s += f" {attribute_name}: {attribute_type};\n" s += "}" serialized.append(s) self.prepared.clear() return "\n\n".join(serialized).strip() def get_types_from_classdef(node: astroid.ClassDef) -> Dict[str, str]: serialized_types: Dict[str, str] = {} for child in node.body: if not isinstance(child, astroid.AnnAssign): continue child_name, child_type = parse_annassign_node(child) serialized_types[child_name] = child_type return serialized_types class ParsedAnnAssign(NamedTuple): attr_name: str attr_type: str def parse_annassign_node(node: astroid.AnnAssign) -> ParsedAnnAssign: def helper(node: astroid.node_classes.NodeNG) -> str: type_value = "UNKNOWN" if isinstance(node, astroid.Name): type_value = TYPE_MAP[node.name] if node.name == "Union": warnings.warn( UserWarning( "Came across an annotation for Union without any indexed types!" " Coercing the annotation to any." ) ) elif isinstance(node, astroid.Subscript): subscript_value = node.value type_format = SUBSCRIPT_FORMAT_MAP[subscript_value.name] type_value = type_format % helper(node.slice.value) elif isinstance(node, astroid.Tuple): inner_types = get_inner_tuple_types(node) delimiter = get_inner_tuple_delimiter(node) if delimiter != "UNKNOWN": type_value = delimiter.join(inner_types) return type_value def get_inner_tuple_types(tuple_node: astroid.Tuple) -> List[str]: # avoid using Set to keep order inner_types: List[str] = [] for child in tuple_node.get_children(): child_type = helper(child) if child_type not in inner_types: inner_types.append(child_type) return inner_types def get_inner_tuple_delimiter(tuple_node: astroid.Tuple) -> str: parent_subscript_name = tuple_node.parent.parent.value.name delimiter = "UNKNOWN" if parent_subscript_name == "Tuple": delimiter = ", " elif parent_subscript_name == "Union": delimiter = " | " return delimiter return ParsedAnnAssign(node.target.name, helper(node.annotation)) def has_dataclass_decorator(decorators: Optional[astroid.Decorators]) -> bool: if not decorators: return False return any( (getattr(decorator.func, "name", None) == "dataclass") if isinstance(decorator, astroid.Call) else decorator.name == "dataclass" for decorator in decorators.nodes ) PK!2bJpy_ts_interfaces/tests.pyfrom astroid import extract_node from itertools import count from py_ts_interfaces import Interface, Parser from py_ts_interfaces.parser import parse_annassign_node, get_types_from_classdef from unittest.mock import patch import pytest @pytest.fixture(scope="module") def interface_qualname(): return f"{Interface.__module__}.{Interface.__qualname__}" TEST_ONE = """ class Foo: pass """ TEST_TWO = """ from py_ts_interfaces import Interface class Foo(Interface): pass """ TEST_THREE = """ from dataclasses import dataclass from py_ts_interfaces import Interface @dataclass class Foo(Interface): pass """ TEST_FOUR = """ from dataclasses import dataclass from py_ts_interfaces import Interface @dataclass class Foo(Interface): pass @dataclass class Bar(Interface): pass class Baz(Interface): pass class Parent: class Child1(Interface): pass @dataclass class Child2(Interface): pass """ TEST_FIVE = """ from dataclasses import dataclass class Interface: pass class Foo(Interface): pass @dataclass class Bar(Interface): pass """ TEST_SIX = """ from dataclasses import dataclass from py_ts_interfaces import Interface @dataclass #@ class Foo(Interface): aaa: str bbb: int ccc: bool ddd = 100 def foo(self) -> None: pass """ TEST_SEVEN = """ from dataclasses import dataclass from py_ts_interfaces import Interface @dataclass #@ class Foo(Interface): def foo(self) -> None: pass aaa: str = 'hello' bbb: int = 5 ccc: bool = True """ TEST_EIGHT = """ from dataclasses import dataclass from py_ts_interfaces import Interface @dataclass class Foo(Interface): aaa: str @dataclass class Foo(Interface): bbb: int """ @pytest.mark.filterwarnings("ignore::UserWarning") @pytest.mark.parametrize( "code, expected_call_count", [ (TEST_ONE, 0), (TEST_TWO, 0), (TEST_THREE, 1), (TEST_FOUR, 3), (TEST_FIVE, 0), (TEST_EIGHT, 1), ], ) def test_parser_parse(code, expected_call_count, interface_qualname): parser = Parser(interface_qualname) with patch("py_ts_interfaces.parser.get_types_from_classdef") as mock_writer: parser.parse(code=code) assert mock_writer.call_count == expected_call_count @pytest.mark.parametrize( "prepared_mocks, expected", [ ({"abc": {"def": "ghi"}}, """interface abc {\n def: ghi;\n}"""), ( {"abc": {"def": "ghi", "jkl": "mno"}}, """interface abc {\n def: ghi;\n jkl: mno;\n}""", ), ({"abc": {}}, """interface abc {\n}"""), ], ) def test_parser_flush(prepared_mocks, expected, interface_qualname): parser = Parser(interface_qualname) parser.prepared = prepared_mocks assert parser.flush() == expected @pytest.mark.filterwarnings("ignore::UserWarning") @pytest.mark.parametrize( "node, expected", [ (extract_node("baz: str"), ("baz", "string")), (extract_node("ace: int"), ("ace", "number")), (extract_node("ace: float"), ("ace", "number")), (extract_node("ace: complex"), ("ace", "number")), (extract_node("ace: bool"), ("ace", "boolean")), (extract_node("ace: Any"), ("ace", "any")), (extract_node("foo: List"), ("foo", "Array")), (extract_node("bar: Tuple"), ("bar", "[any]")), (extract_node("foo: List[str]"), ("foo", "Array")), (extract_node("bar: Tuple[str, int]"), ("bar", "[string, number]")), (extract_node("baz: Optional[str]"), ("baz", "string | null")), (extract_node("ace: Optional[int]"), ("ace", "number | null")), (extract_node("ace: Optional[float]"), ("ace", "number | null")), (extract_node("ace: Optional[complex]"), ("ace", "number | null")), (extract_node("ace: Optional[bool]"), ("ace", "boolean | null")), (extract_node("ace: Optional[Any]"), ("ace", "any | null")), ( extract_node("bar: Optional[Tuple[str, int]]"), ("bar", "[string, number] | null"), ), ( extract_node("bar: Tuple[List[Optional[Tuple[str, int]]], str, int]"), ("bar", "[Array<[string, number] | null>, string, number]"), ), (extract_node("lol: Union[str, int, float]"), ("lol", "string | number")), (extract_node("lol: Union"), ("lol", "any")), ], ) def test_parse_annassign_node(node, expected): assert parse_annassign_node(node) == expected @pytest.mark.parametrize("code, expected_call_count", [(TEST_SIX, 0)]) def test_get_types_from_classdef(code, expected_call_count): classdef = extract_node(code) with patch("py_ts_interfaces.parser.parse_annassign_node") as annassign_parser: k, v = count(0, 2), count(1, 2) annassign_parser.side_effect = lambda x: (str(next(k)), str(next(v))) result = get_types_from_classdef(classdef) assert result == {"0": "1", "2": "3", "4": "5"} assert annassign_parser.call_count == 3 PK!Hn08>1py_ts_interfaces-0.1.0.dist-info/entry_points.txtN+I/N.,()*-)+I-JKLN--/)G%dZ&fqqPK!G8C<<(py_ts_interfaces-0.1.0.dist-info/LICENSEMIT License Copyright (c) 2019 Christopher Sabater Cordero Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTU&py_ts_interfaces-0.1.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!Hxp )py_ts_interfaces-0.1.0.dist-info/METADATAYmsܶ_FN'38"ZKX=#H"` P' y2$,gd.|PզTj*nDWN5s) kI'DgmYʦQY-nTx׹ ibRVBVqA)YډLʅU R~\*~1rmv ,LIf\5f-ot*}'ѿTBK61ihYxf*KՈC@x7QTd8SїQ^ՍoCrkvb:oώA 97*gۿ1F⍬-BC]|VO1:_H^$݋ W7SqdTk{Cr\C+7 n>r:7*vN9cJ5X$>RjJ$mE5$/XȲB;mKtP4RBǕu(XXiɍ@!vD"CaKTU:DM{\ Yׅ΂+$̈́cZ))f2VНW_UB;3$ҙOFt>[kz]8HV*E*C2gj+]Z}Q~#sp&xlK E:K#kUnt\l='@T5j)F:.&*-1b/fj_>Rvb.=lmndHBu[p)v)q`l 58F!8=- kHF RX{b{QytlyBbKŢKr 2I. ܫ_KNRaq\Mx5-U+`P3bl-RR. 9~ BֆCJ03=>P&O팗DXtyJÃF]C/N6(b!+ %u,mMR1MTvčV+]Ш4rjްl=~\on: VUݦi$S1]Ak'4S#WTTS~VFzJQ([=sC$ZH+r%lV9> 0H ɻ.QƱh* WѡH4K>$G'ȜAPrpZ j'3 ` rM֭I'u6E8J|ìQ=F10j (̊2\6 ȬE:11]PT[~NߤJj=gԎJH$y]5[FlaެAA.B."I,+4 /Ӓl<7LB{Pp Oآ8g%'_ +б7CBLG~uD^j˙j^EHe'UΔ)1 ThLf.^fV/(934o8ZWR7=:m t#nMk& LZsDfd*JG:s)5 2&\*kåh t ΙC Bj3U6q6s/f7])!OoL׭L`",xdՅhd.+Jc9uQk^:1"%VfpHk% /1#z3|pJfLs@z*ADDߏXXd3Y^+xY;Yeȴ%k{S$IEXZ?zgTj}s<8> CbaL3a<1zp[wpVK "3dXӃ)d8ڃxYZ 3(9m< 8fDJID8G )4nZ ;8;T>ʢ1(NxҙǾ' iGΗ8axX@27dhwSp/chV`$H^T4" 84 E2cl%fVyˠ24,5#r C#,0xC. g\쵠}%Ĉ@ M:z5q|I+dLA- R<EҞfB%D ,;Wf#7)2TT~–BЦ<D.y玍 cd +!N:܁e~hKD@@ Sm2x2Gdޢ)o;q4=bK5^' z\EA[SiK "X|xDLqA|xT;15 ȃYg:7U%Bk<bdiR ]W4~lKs^ mCWBҌȂX`CpB"+"WQ +~_囖p`xBcz2F 4qd}~Es"LhQCYi{QPK!H񛽾'py_ts_interfaces-0.1.0.dist-info/RECORD͹@|f n"^Ť Y$VYTvBGeM.F$-ډꋘAP%ҀLWAt瞀" %3_dR/f; tǟF_I`t΢}cāx{mYBy[4㱵/jl;qCX.3~ ;I/"w=u7I0De_xGBM`i;I7 2gPV߭Uc9_n֡io:(ҡ'o#r>5X`Q mW?`!,y CQ(ρ B?ݻgŭQ-z:뤊5cc0)1%LER$(K^y8F֔sPK!3CPPpy_ts_interfaces/__init__.pyPK!3&7 7 py_ts_interfaces/cli.pyPK! py_ts_interfaces/parser.pyPK!2bJpy_ts_interfaces/tests.pyPK!Hn08>12py_ts_interfaces-0.1.0.dist-info/entry_points.txtPK!G8C<<(2py_ts_interfaces-0.1.0.dist-info/LICENSEPK!HڽTU& 7py_ts_interfaces-0.1.0.dist-info/WHEELPK!Hxp )7py_ts_interfaces-0.1.0.dist-info/METADATAPK!H񛽾'YCpy_ts_interfaces-0.1.0.dist-info/RECORDPK \E