├── .gitignore ├── LICENSE ├── README.md ├── py_to_ts_interfaces ├── __init__.py ├── __main__.py ├── enums.py ├── file_io.py ├── interfaces.py ├── strings.py ├── type_converting.py └── utils.py ├── pylintrc ├── pyproject.toml ├── setup.cfg └── tests ├── __init__.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # celery beat schedule file 88 | celerybeat-schedule 89 | 90 | # SageMath parsed files 91 | *.sage.py 92 | 93 | # Environments 94 | .env 95 | .venv 96 | env/ 97 | venv/ 98 | ENV/ 99 | env.bak/ 100 | venv.bak/ 101 | 102 | # Spyder project settings 103 | .spyderproject 104 | .spyproject 105 | 106 | # Rope project settings 107 | .ropeproject 108 | 109 | # mkdocs documentation 110 | /site 111 | 112 | # mypy 113 | .mypy_cache/ 114 | .dmypy.json 115 | dmypy.json 116 | 117 | # Pyre type checker 118 | .pyre/ 119 | 120 | data/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # py-to-ts-interfaces 2 | ### Python to TypeScript Interfaces 3 | 4 | ## What is this? 5 | 6 | A script for converting Python dataclasses with type annotations to TypeScript interfaces. This is a very similar 7 | project to [py-ts-interfaces](https://github.com/cs-cordero/py-ts-interfaces), and only exists because that project 8 | does not currently support enums. This is a utility for another project I am working on, and has the 9 | additional benefit of allowing me to generate the TypeScript output in compliance with my eslint configuration. This 10 | is a much more primitive approach compared to [py-ts-interfaces](https://github.com/cs-cordero/py-ts-interfaces) which 11 | comes with certain limitations (see [Usage](#Usage) for details). 12 | 13 | ## Installation 14 | 15 | ``` 16 | python --version # requires 3.9+ 17 | pip install py-to-ts-interfaces 18 | ``` 19 | 20 | ## Motivation 21 | 22 | Just like [py-ts-interfaces](https://github.com/cs-cordero/py-ts-interfaces), this script is intended for cases 23 | where a web application is composed of a Python server and a TypeScript client. Setting up a language translator 24 | like this means that it is possible to define the message schemas once (in Python), and then guarantee that the 25 | TypeScript message schemas are in sync with the Python ones. This avoids the annoying task of maintaining two 26 | definition sets, and more importantly, bugs caused by forgetting to update both interfaces. 27 | 28 | ## Usage 29 | 30 | This script takes a single input folder, and requires that all python files inside only contain the following: 31 | - Module imports 32 | - Newlines 33 | - Spaces 34 | - [Dataclasses](https://docs.python.org/3/library/dataclasses.html) 35 | - Enums 36 | - String definitions 37 | 38 | If a dataclass contains an enum, the enum definition must be in the same folder also. 39 | This script also supports nullable types (see `MyNullableInterface` in the section below). 40 | Functions in Enum definitions will be ignored (e.g. a `__str__` override). 41 | 42 | ### Example 43 | 44 | 1. Write your Python definitions. 45 | 46 | ```python 47 | from dataclasses import dataclass 48 | from enum import Enum 49 | from typing import Final, Union, List, Dict, Optional 50 | 51 | class MyEnum(Enum): 52 | FIRST = "Number One" 53 | SECOND = "Number Two" 54 | 55 | def __str__(self): 56 | return self.value 57 | 58 | CONSTANT_STRING: Final = "example" 59 | OTHER_STRING = "another example" 60 | 61 | @dataclass(frozen=True) 62 | class MyInterface: 63 | field: MyEnum 64 | 65 | @dataclass(frozen=True) 66 | class MyNullableInterface: 67 | field: Union[MyInterface, None] = None 68 | otherField: Optional[MyInterface] = None 69 | 70 | @dataclass(frozen=True) 71 | class MyInterface2: 72 | strange_type: Optional[List[int]] 73 | other_type: List[str] 74 | dict_type: Dict[int, Dict[str, MyEnum]] 75 | 76 | ``` 77 | 78 | 2. In your shell, run the included command and pass in the path of the folder containing the files you want to convert, 79 | and the path to the folder that the output should be written to. If the output folder path does not exist then it 80 | will be created automatically. 81 | ``` 82 | $ py-ts-interfaces example_folder output_folder 83 | ``` 84 | 85 | 3. The resulting file will look like this: 86 | ```typescript 87 | export enum MyEnum { 88 | FIRST = 'Number One', 89 | SECOND = 'Number Two', 90 | } 91 | 92 | export const CONSTANT_STRING = 'example'; 93 | export const OTHER_STRING = 'another example'; 94 | 95 | export interface MyInterface { 96 | field: MyEnum; 97 | } 98 | 99 | export interface MyNullableInterface { 100 | field?: MyInterface; 101 | otherField?: MyInterface; 102 | } 103 | 104 | export interface MyInterface2 { 105 | strangeType?: number[]; 106 | otherType: string[]; 107 | dictType: Record>; 108 | } 109 | 110 | ``` 111 | 112 | ## Supported Type Mappings 113 | 114 | | Python | Typescript | 115 | |:------------:|:------------:| 116 | | str | string | 117 | | int | number | 118 | | float | number | 119 | | complex | number | 120 | | bool | boolean | 121 | | List[T] | T[] | 122 | | Dict[T, P] | Record | 123 | 124 | Where T and P are one of the listed supported types (this includes nested Dicts), or enums. -------------------------------------------------------------------------------- /py_to_ts_interfaces/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Syndallic/py-to-ts-interfaces/45d9dfb47252bf399951e4e7e71514bfdb8586c5/py_to_ts_interfaces/__init__.py -------------------------------------------------------------------------------- /py_to_ts_interfaces/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from typing import Union 4 | import argparse 5 | 6 | from py_to_ts_interfaces.enums import EnumDefinition 7 | from py_to_ts_interfaces.file_io import write_file, read_file 8 | from py_to_ts_interfaces.interfaces import InterfaceDefinition 9 | from py_to_ts_interfaces.strings import StringDefinition 10 | from py_to_ts_interfaces.utils import is_class_definition, is_string_definition 11 | 12 | 13 | def python_to_typescript_file(python_code: str) -> str: 14 | """ 15 | Convert python enum and dataclass definitions to equivalent typescript code. 16 | 17 | :param python_code: Python code containing only enums and dataclasses. 18 | :return: Equivalent typescript code. 19 | """ 20 | # initial processing (remove superfluous lines) 21 | lines = python_code.splitlines() 22 | lines = [line for line in lines if line and not line.isspace() and not line.startswith(("from ", "#", "@"))] 23 | 24 | # group the lines for each enum/class definition together 25 | definition_groups: list[list[str]] = [] 26 | for line in lines: 27 | if is_class_definition(line) or is_string_definition(line): 28 | definition_groups.append([]) 29 | definition_groups[-1].append(line) 30 | 31 | # convert each group into either an EnumDefinition or InterfaceDefinition object 32 | processed_definitions: list[Union[EnumDefinition, InterfaceDefinition, StringDefinition]] = [] 33 | for definition in definition_groups: 34 | if definition[0].endswith("(Enum):"): 35 | processed_definitions.append(EnumDefinition(definition)) 36 | elif definition[0].endswith("\""): 37 | processed_definitions.append(StringDefinition(definition)) 38 | else: 39 | processed_definitions.append(InterfaceDefinition(definition)) 40 | 41 | # construct final output 42 | typescript_output = "" 43 | for i, processed_definition in enumerate(processed_definitions): 44 | typescript_output += "{}\n".format(processed_definition.get_typescript()) 45 | # Want consecutive string definitions to be next to each other 46 | if not (len(processed_definitions) >= i + 1 and 47 | isinstance(processed_definition, StringDefinition) and 48 | isinstance(processed_definitions[i + 1], StringDefinition)): 49 | typescript_output += "\n" 50 | typescript_output = typescript_output.strip("\n") 51 | # add just one newline at the end 52 | typescript_output += "\n" 53 | 54 | return typescript_output 55 | 56 | 57 | def python_to_typescript_folder(input_path: str, output_path: str) -> None: 58 | """ 59 | Convert all python files in input directory to typescript files in output directory. Each output file has the 60 | same name as its python source (with the file extension changed to 'ts'). 61 | 62 | :param input_path: A full or relative path to a folder containing .py files. 63 | :param output_path: A full or relative path to a folder which may not exist. 64 | """ 65 | for file in os.listdir(input_path): 66 | if file.endswith(".py") and file != "__init__.py": 67 | file_contents = read_file(os.path.join(input_path, file)) 68 | 69 | typescript_output = python_to_typescript_file(file_contents) 70 | 71 | write_file(typescript_output, os.path.join(output_path, file[:-3] + ".ts")) 72 | 73 | 74 | def main(): 75 | """Main script.""" 76 | parser = argparse.ArgumentParser() 77 | parser.add_argument("input_folder", help="The path to the folder of python files to be converted") 78 | parser.add_argument("output_folder", help="The path to the folder to output the typescript files to") 79 | args = parser.parse_args() 80 | 81 | python_to_typescript_folder(args.input_folder, args.output_folder) 82 | 83 | 84 | if __name__ == "__main__": 85 | sys.exit(main()) 86 | -------------------------------------------------------------------------------- /py_to_ts_interfaces/enums.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | 4 | class EnumElement: 5 | """Represent one element of an enum.""" 6 | name: str 7 | value: str 8 | 9 | def __init__(self, line: str): 10 | name_and_value = line.strip().split(" = ") 11 | self.name = name_and_value[0] 12 | self.value = name_and_value[1].strip("\"") 13 | 14 | def get_typescript(self) -> str: 15 | """Return the element in typescript syntax (including indentation).""" 16 | return " {0} = \'{1}\',".format(self.name, self.value) 17 | 18 | 19 | class EnumDefinition: 20 | """Represent a python/typescript enum.""" 21 | name: str 22 | elements: List[EnumElement] 23 | 24 | def __init__(self, definition: List[str]): 25 | definition = [line for line in definition if 26 | not line.startswith(" def") and 27 | not line.startswith(" ") 28 | ] 29 | 30 | self.name = definition[0].removeprefix("class ").removesuffix("(Enum):") 31 | self.elements = [EnumElement(line) for line in definition[1:]] 32 | 33 | def get_typescript(self) -> str: 34 | """Return the enum in typescript syntax (including indentation).""" 35 | typescript_string = "export enum {0} {{\n".format(self.name) 36 | for element in self.elements: 37 | typescript_string += "{}\n".format(element.get_typescript()) 38 | typescript_string += "}" 39 | return typescript_string 40 | -------------------------------------------------------------------------------- /py_to_ts_interfaces/file_io.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def read_file(file_path: str) -> str: 5 | """Read content of file provided.""" 6 | with open(file_path, "r", encoding="utf-8") as reader: 7 | return reader.read() 8 | 9 | 10 | def write_file(to_write: str, file_path: str) -> None: 11 | """Write input string to file.""" 12 | folder_path = os.path.dirname(file_path) 13 | if folder_path and not os.path.isdir(folder_path): 14 | os.makedirs(folder_path) 15 | with open(file_path, "w+", encoding="utf-8") as writer: 16 | writer.write(to_write) 17 | -------------------------------------------------------------------------------- /py_to_ts_interfaces/interfaces.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | from py_to_ts_interfaces.type_converting import python_to_typescript_type 4 | from py_to_ts_interfaces.utils import to_camel_case 5 | 6 | 7 | class InterfaceField: 8 | """Represent a dataclass field.""" 9 | name: str 10 | python_type: str 11 | is_nullable: bool 12 | 13 | def __init__(self, line: str): 14 | self.is_nullable = line.endswith(" = None") or "Union[" in line or "Optional[" in line 15 | line = line.removesuffix(" = None") 16 | 17 | self.name, self.python_type = self.get_name_and_type(line) 18 | 19 | @staticmethod 20 | def get_name_and_type(line: str) -> Tuple[str, str]: 21 | """Take a line like "field_name: Union[None, int]" and return ("fieldName", "int")""" 22 | name, python_type = line.strip().split(": ") 23 | name = to_camel_case(name) 24 | if "Union[" in python_type: 25 | python_type = python_type.removeprefix("Union[None, ").removeprefix("Union[") 26 | python_type = python_type.removesuffix("]").removesuffix(", None") 27 | if "Optional[" in python_type: 28 | python_type = python_type.removeprefix("Optional[") 29 | python_type = python_type.removesuffix("]").removesuffix(", None") 30 | return name, python_type 31 | 32 | def get_typescript(self) -> str: 33 | """Return the field in typescript syntax (including indentation).""" 34 | ts_name = self.name 35 | if self.is_nullable: 36 | ts_name += "?" 37 | return " {0}: {1};".format(ts_name, python_to_typescript_type(self.python_type)) 38 | 39 | 40 | class InterfaceDefinition: 41 | """Represent a python dataclass/typescript interface.""" 42 | name: str 43 | fields: List[InterfaceField] 44 | 45 | def __init__(self, definition: List[str]): 46 | self.name = definition[0].removeprefix("class ").strip(":") 47 | self.fields = [InterfaceField(line) for line in definition[1:]] 48 | 49 | def get_typescript(self) -> str: 50 | """Return the entire interface in typescript syntax (including indentation).""" 51 | typescript_string = "export interface {0} {{\n".format(self.name) 52 | for field in self.fields: 53 | typescript_string += "{}\n".format(field.get_typescript()) 54 | typescript_string += "}" 55 | return typescript_string 56 | -------------------------------------------------------------------------------- /py_to_ts_interfaces/strings.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List 3 | 4 | 5 | class StringDefinition: 6 | """Represent a string definition.""" 7 | name: str 8 | value: str 9 | 10 | def __init__(self, definition: List[str]): 11 | matches = re.match("([a-zA-Z_]+).* = \"(.*)\"", definition[0]) 12 | if matches: 13 | self.name = matches.group(1) 14 | self.value = matches.group(2) 15 | 16 | def get_typescript(self) -> str: 17 | """Return the string definition in typescript syntax.""" 18 | typescript_string = "export const {0} = '{1}';".format(self.name, self.value) 19 | return typescript_string 20 | -------------------------------------------------------------------------------- /py_to_ts_interfaces/type_converting.py: -------------------------------------------------------------------------------- 1 | # Manually supporting dict types scales pretty badly, so the python_to_typescript_type function handles them 2 | # programmatically for the sake of sanity. 3 | python_to_typescript_type_map = { 4 | "str": "string", 5 | "int": "number", 6 | "float": "number", 7 | "complex": "number", 8 | "bool": "boolean", 9 | "List[str]": "string[]", 10 | "List[int]": "number[]", 11 | "List[float]": "number[]", 12 | "List[complex]": "number[]", 13 | "List[bool]": "boolean[]", 14 | } 15 | 16 | 17 | def python_to_typescript_type(python_type: str) -> str: 18 | """ 19 | Map python type to an equivalent typescript type. 20 | 21 | :param python_type: A python type like 'str' or 'int'. 22 | :return: An equivalent typescript type. If there is no known mapping for the input, then it is returned as-is for 23 | enum support. 24 | """ 25 | try: 26 | return python_to_typescript_type_map[python_type] 27 | except KeyError: 28 | if python_type.startswith("Dict["): 29 | python_type = python_type.removeprefix("Dict[").removesuffix("]").replace(" ", "") 30 | # This part handles nested dicts 31 | py_type_1, py_type_2 = python_type.split(",", 1) 32 | ts_type_1 = python_to_typescript_type(py_type_1) 33 | ts_type_2 = python_to_typescript_type(py_type_2) 34 | return f"Record<{ts_type_1}, {ts_type_2}>" 35 | elif python_type.startswith("List["): 36 | # This means the list contains an unknown type - likely an enum 37 | python_type = python_type.removeprefix("List[").removesuffix("]") 38 | return f"{python_type}[]" 39 | else: 40 | # This should mean it is an enum 41 | return python_type 42 | -------------------------------------------------------------------------------- /py_to_ts_interfaces/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def to_camel_case(snake_str: str) -> str: 5 | """ 6 | Convert a snake_case string to camelCase. 7 | 8 | :param snake_str: The input in snake_case. 9 | :return: The input, but in camelCase. 10 | """ 11 | components = snake_str.split('_') 12 | # We capitalize the first letter of each component except the first one 13 | # with the 'title' method and join them together. 14 | return components[0] + ''.join(x.title() for x in components[1:]) 15 | 16 | 17 | def is_class_definition(line: str) -> bool: 18 | """ 19 | Check if the given string is a class definition, e.g. "class MyInterface:" 20 | 21 | :param line: The string to check (should be one line of code). 22 | :return: True if the given string is a class definition. 23 | """ 24 | return line.startswith("class ") 25 | 26 | 27 | def is_string_definition(line: str) -> bool: 28 | """ 29 | Check if the given string is a string definition. Ignores type hints such as Final. 30 | e.g. CONSTANT_STRING: Final = "example" 31 | 32 | :param line: The string to check (should be one line of code). 33 | :return: True if the given string is a string definition. 34 | """ 35 | return re.match("[a-zA-Z_]+.* = \".*\"", line) is not None 36 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-allow-list= 7 | 8 | # A comma-separated list of package or module names from where C extensions may 9 | # be loaded. Extensions are loading into the active Python interpreter and may 10 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 11 | # for backward compatibility.) 12 | extension-pkg-whitelist= 13 | 14 | # Specify a score threshold to be exceeded before program exits with error. 15 | fail-under=10.0 16 | 17 | # Files or directories to be skipped. They should be base names, not paths. 18 | ignore=CVS 19 | 20 | # Files or directories matching the regex patterns are skipped. The regex 21 | # matches against base names, not paths. 22 | ignore-patterns= 23 | 24 | # Python code to execute, usually for sys.path manipulation such as 25 | # pygtk.require(). 26 | #init-hook= 27 | 28 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 29 | # number of processors available to use. 30 | jobs=1 31 | 32 | # Control the amount of potential inferred values when inferring a single 33 | # object. This can help the performance when dealing with large functions or 34 | # complex, nested conditions. 35 | limit-inference-results=100 36 | 37 | # List of plugins (as comma separated values of python module names) to load, 38 | # usually to register additional checkers. 39 | load-plugins= 40 | 41 | # Pickle collected data for later comparisons. 42 | persistent=yes 43 | 44 | # When enabled, pylint would attempt to guess common misconfiguration and emit 45 | # user-friendly hints instead of false-positive error messages. 46 | suggestion-mode=yes 47 | 48 | # Allow loading of arbitrary C extensions. Extensions are imported into the 49 | # active Python interpreter and may run arbitrary code. 50 | unsafe-load-any-extension=no 51 | 52 | 53 | [MESSAGES CONTROL] 54 | 55 | # Only show warnings with the listed confidence levels. Leave empty to show 56 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 57 | confidence= 58 | 59 | # Disable the message, report, category or checker with the given id(s). You 60 | # can either give multiple identifiers separated by comma (,) or put this 61 | # option multiple times (only on the command line, not in the configuration 62 | # file where it should appear only once). You can also use "--disable=all" to 63 | # disable everything first and then reenable specific checks. For example, if 64 | # you want to run only the similarities checker, you can use "--disable=all 65 | # --enable=similarities". If you want to run only the classes checker, but have 66 | # no Warning level messages displayed, use "--disable=all --enable=classes 67 | # --disable=W". 68 | disable=print-statement, 69 | parameter-unpacking, 70 | unpacking-in-except, 71 | old-raise-syntax, 72 | backtick, 73 | long-suffix, 74 | old-ne-operator, 75 | old-octal-literal, 76 | import-star-module-level, 77 | non-ascii-bytes-literal, 78 | raw-checker-failed, 79 | bad-inline-option, 80 | locally-disabled, 81 | file-ignored, 82 | suppressed-message, 83 | useless-suppression, 84 | deprecated-pragma, 85 | use-symbolic-message-instead, 86 | apply-builtin, 87 | basestring-builtin, 88 | buffer-builtin, 89 | cmp-builtin, 90 | coerce-builtin, 91 | execfile-builtin, 92 | file-builtin, 93 | long-builtin, 94 | raw_input-builtin, 95 | reduce-builtin, 96 | standarderror-builtin, 97 | unicode-builtin, 98 | xrange-builtin, 99 | coerce-method, 100 | delslice-method, 101 | getslice-method, 102 | setslice-method, 103 | no-absolute-import, 104 | old-division, 105 | dict-iter-method, 106 | dict-view-method, 107 | next-method-called, 108 | metaclass-assignment, 109 | indexing-exception, 110 | raising-string, 111 | reload-builtin, 112 | oct-method, 113 | hex-method, 114 | nonzero-method, 115 | cmp-method, 116 | input-builtin, 117 | round-builtin, 118 | intern-builtin, 119 | unichr-builtin, 120 | map-builtin-not-iterating, 121 | zip-builtin-not-iterating, 122 | range-builtin-not-iterating, 123 | filter-builtin-not-iterating, 124 | using-cmp-argument, 125 | eq-without-hash, 126 | div-method, 127 | idiv-method, 128 | rdiv-method, 129 | exception-message-attribute, 130 | invalid-str-codec, 131 | sys-max-int, 132 | bad-python3-import, 133 | deprecated-string-function, 134 | deprecated-str-translate-call, 135 | deprecated-itertools-function, 136 | deprecated-types-field, 137 | next-method-defined, 138 | dict-items-not-iterating, 139 | dict-keys-not-iterating, 140 | dict-values-not-iterating, 141 | deprecated-operator-function, 142 | deprecated-urllib-function, 143 | xreadlines-attribute, 144 | deprecated-sys-function, 145 | exception-escape, 146 | comprehension-escape, 147 | missing-module-docstring, 148 | too-few-public-methods, 149 | no-else-return, 150 | too-many-arguments 151 | 152 | # Enable the message, report, category or checker with the given id(s). You can 153 | # either give multiple identifier separated by comma (,) or put this option 154 | # multiple time (only on the command line, not in the configuration file where 155 | # it should appear only once). See also the "--disable" option for examples. 156 | enable=c-extension-no-member 157 | 158 | 159 | [REPORTS] 160 | 161 | # Python expression which should return a score less than or equal to 10. You 162 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 163 | # which contain the number of messages in each category, as well as 'statement' 164 | # which is the total number of statements analyzed. This score is used by the 165 | # global evaluation report (RP0004). 166 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 167 | 168 | # Template used to display messages. This is a python new-style format string 169 | # used to format the message information. See doc for all details. 170 | #msg-template= 171 | 172 | # Set the output format. Available formats are text, parseable, colorized, json 173 | # and msvs (visual studio). You can also give a reporter class, e.g. 174 | # mypackage.mymodule.MyReporterClass. 175 | output-format=text 176 | 177 | # Tells whether to display a full report or only the messages. 178 | reports=no 179 | 180 | # Activate the evaluation score. 181 | score=yes 182 | 183 | 184 | [REFACTORING] 185 | 186 | # Maximum number of nested blocks for function / method body 187 | max-nested-blocks=5 188 | 189 | # Complete name of functions that never returns. When checking for 190 | # inconsistent-return-statements if a never returning function is called then 191 | # it will be considered as an explicit return statement and no message will be 192 | # printed. 193 | never-returning-functions=sys.exit 194 | 195 | 196 | [BASIC] 197 | 198 | # Naming style matching correct argument names. 199 | argument-naming-style=snake_case 200 | 201 | # Regular expression matching correct argument names. Overrides argument- 202 | # naming-style. 203 | #argument-rgx= 204 | 205 | # Naming style matching correct attribute names. 206 | attr-naming-style=snake_case 207 | 208 | # Regular expression matching correct attribute names. Overrides attr-naming- 209 | # style. 210 | #attr-rgx= 211 | 212 | # Bad variable names which should always be refused, separated by a comma. 213 | bad-names=foo, 214 | bar, 215 | baz, 216 | toto, 217 | tutu, 218 | tata 219 | 220 | # Bad variable names regexes, separated by a comma. If names match any regex, 221 | # they will always be refused 222 | bad-names-rgxs= 223 | 224 | # Naming style matching correct class attribute names. 225 | class-attribute-naming-style=any 226 | 227 | # Regular expression matching correct class attribute names. Overrides class- 228 | # attribute-naming-style. 229 | #class-attribute-rgx= 230 | 231 | # Naming style matching correct class constant names. 232 | class-const-naming-style=UPPER_CASE 233 | 234 | # Regular expression matching correct class constant names. Overrides class- 235 | # const-naming-style. 236 | #class-const-rgx= 237 | 238 | # Naming style matching correct class names. 239 | class-naming-style=PascalCase 240 | 241 | # Regular expression matching correct class names. Overrides class-naming- 242 | # style. 243 | #class-rgx= 244 | 245 | # Naming style matching correct constant names. 246 | const-naming-style=UPPER_CASE 247 | 248 | # Regular expression matching correct constant names. Overrides const-naming- 249 | # style. 250 | #const-rgx= 251 | 252 | # Minimum line length for functions/classes that require docstrings, shorter 253 | # ones are exempt. 254 | docstring-min-length=-1 255 | 256 | # Naming style matching correct function names. 257 | function-naming-style=snake_case 258 | 259 | # Regular expression matching correct function names. Overrides function- 260 | # naming-style. 261 | #function-rgx= 262 | 263 | # Good variable names which should always be accepted, separated by a comma. 264 | good-names=i, 265 | j, 266 | k, 267 | ex, 268 | Run, 269 | _ 270 | 271 | # Good variable names regexes, separated by a comma. If names match any regex, 272 | # they will always be accepted 273 | good-names-rgxs= 274 | 275 | # Include a hint for the correct naming format with invalid-name. 276 | include-naming-hint=no 277 | 278 | # Naming style matching correct inline iteration names. 279 | inlinevar-naming-style=any 280 | 281 | # Regular expression matching correct inline iteration names. Overrides 282 | # inlinevar-naming-style. 283 | #inlinevar-rgx= 284 | 285 | # Naming style matching correct method names. 286 | method-naming-style=snake_case 287 | 288 | # Regular expression matching correct method names. Overrides method-naming- 289 | # style. 290 | #method-rgx= 291 | 292 | # Naming style matching correct module names. 293 | module-naming-style=snake_case 294 | 295 | # Regular expression matching correct module names. Overrides module-naming- 296 | # style. 297 | #module-rgx= 298 | 299 | # Colon-delimited sets of names that determine each other's naming style when 300 | # the name regexes allow several styles. 301 | name-group= 302 | 303 | # Regular expression which should only match function or class names that do 304 | # not require a docstring. 305 | no-docstring-rgx=^_ 306 | 307 | # List of decorators that produce properties, such as abc.abstractproperty. Add 308 | # to this list to register other decorators that produce valid properties. 309 | # These decorators are taken in consideration only for invalid-name. 310 | property-classes=abc.abstractproperty 311 | 312 | # Naming style matching correct variable names. 313 | variable-naming-style=snake_case 314 | 315 | # Regular expression matching correct variable names. Overrides variable- 316 | # naming-style. 317 | #variable-rgx= 318 | 319 | 320 | [FORMAT] 321 | 322 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 323 | expected-line-ending-format= 324 | 325 | # Regexp for a line that is allowed to be longer than the limit. 326 | ignore-long-lines=^\s*(# )??$ 327 | 328 | # Number of spaces of indent required inside a hanging or continued line. 329 | indent-after-paren=4 330 | 331 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 332 | # tab). 333 | indent-string=' ' 334 | 335 | # Maximum number of characters on a single line. 336 | max-line-length=120 337 | 338 | # Maximum number of lines in a module. 339 | max-module-lines=1000 340 | 341 | # Allow the body of a class to be on the same line as the declaration if body 342 | # contains single statement. 343 | single-line-class-stmt=no 344 | 345 | # Allow the body of an if to be on the same line as the test if there is no 346 | # else. 347 | single-line-if-stmt=no 348 | 349 | 350 | [LOGGING] 351 | 352 | # The type of string formatting that logging methods do. `old` means using % 353 | # formatting, `new` is for `{}` formatting. 354 | logging-format-style=old 355 | 356 | # Logging modules to check that the string format arguments are in logging 357 | # function parameter format. 358 | logging-modules=logging 359 | 360 | 361 | [MISCELLANEOUS] 362 | 363 | # List of note tags to take in consideration, separated by a comma. 364 | notes=FIXME, 365 | XXX, 366 | TODO 367 | 368 | # Regular expression of note tags to take in consideration. 369 | #notes-rgx= 370 | 371 | 372 | [SIMILARITIES] 373 | 374 | # Ignore comments when computing similarities. 375 | ignore-comments=yes 376 | 377 | # Ignore docstrings when computing similarities. 378 | ignore-docstrings=yes 379 | 380 | # Ignore imports when computing similarities. 381 | ignore-imports=no 382 | 383 | # Minimum lines number of a similarity. 384 | min-similarity-lines=4 385 | 386 | 387 | [SPELLING] 388 | 389 | # Limits count of emitted suggestions for spelling mistakes. 390 | max-spelling-suggestions=4 391 | 392 | # Spelling dictionary name. Available dictionaries: none. To make it work, 393 | # install the 'python-enchant' package. 394 | spelling-dict= 395 | 396 | # List of comma separated words that should not be checked. 397 | spelling-ignore-words= 398 | 399 | # A path to a file that contains the private dictionary; one word per line. 400 | spelling-private-dict-file= 401 | 402 | # Tells whether to store unknown words to the private dictionary (see the 403 | # --spelling-private-dict-file option) instead of raising a message. 404 | spelling-store-unknown-words=no 405 | 406 | 407 | [STRING] 408 | 409 | # This flag controls whether inconsistent-quotes generates a warning when the 410 | # character used as a quote delimiter is used inconsistently within a module. 411 | check-quote-consistency=no 412 | 413 | # This flag controls whether the implicit-str-concat should generate a warning 414 | # on implicit string concatenation in sequences defined over several lines. 415 | check-str-concat-over-line-jumps=no 416 | 417 | 418 | [TYPECHECK] 419 | 420 | # List of decorators that produce context managers, such as 421 | # contextlib.contextmanager. Add to this list to register other decorators that 422 | # produce valid context managers. 423 | contextmanager-decorators=contextlib.contextmanager 424 | 425 | # List of members which are set dynamically and missed by pylint inference 426 | # system, and so shouldn't trigger E1101 when accessed. Python regular 427 | # expressions are accepted. 428 | generated-members= 429 | 430 | # Tells whether missing members accessed in mixin class should be ignored. A 431 | # mixin class is detected if its name ends with "mixin" (case insensitive). 432 | ignore-mixin-members=yes 433 | 434 | # Tells whether to warn about missing members when the owner of the attribute 435 | # is inferred to be None. 436 | ignore-none=yes 437 | 438 | # This flag controls whether pylint should warn about no-member and similar 439 | # checks whenever an opaque object is returned when inferring. The inference 440 | # can return multiple potential results while evaluating a Python object, but 441 | # some branches might not be evaluated, which results in partial inference. In 442 | # that case, it might be useful to still emit no-member and other checks for 443 | # the rest of the inferred objects. 444 | ignore-on-opaque-inference=yes 445 | 446 | # List of class names for which member attributes should not be checked (useful 447 | # for classes with dynamically set attributes). This supports the use of 448 | # qualified names. 449 | ignored-classes=optparse.Values,thread._local,_thread._local 450 | 451 | # List of module names for which member attributes should not be checked 452 | # (useful for modules/projects where namespaces are manipulated during runtime 453 | # and thus existing member attributes cannot be deduced by static analysis). It 454 | # supports qualified module names, as well as Unix pattern matching. 455 | ignored-modules= 456 | 457 | # Show a hint with possible names when a member name was not found. The aspect 458 | # of finding the hint is based on edit distance. 459 | missing-member-hint=yes 460 | 461 | # The minimum edit distance a name should have in order to be considered a 462 | # similar match for a missing member name. 463 | missing-member-hint-distance=1 464 | 465 | # The total number of similar names that should be taken in consideration when 466 | # showing a hint for a missing member. 467 | missing-member-max-choices=1 468 | 469 | # List of decorators that change the signature of a decorated function. 470 | signature-mutators= 471 | 472 | 473 | [VARIABLES] 474 | 475 | # List of additional names supposed to be defined in builtins. Remember that 476 | # you should avoid defining new builtins when possible. 477 | additional-builtins= 478 | 479 | # Tells whether unused global variables should be treated as a violation. 480 | allow-global-unused-variables=yes 481 | 482 | # List of names allowed to shadow builtins 483 | allowed-redefined-builtins= 484 | 485 | # List of strings which can identify a callback function by name. A callback 486 | # name must start or end with one of those strings. 487 | callbacks=cb_, 488 | _cb 489 | 490 | # A regular expression matching the name of dummy variables (i.e. expected to 491 | # not be used). 492 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 493 | 494 | # Argument names that match this expression will be ignored. Default to name 495 | # with leading underscore. 496 | ignored-argument-names=_.*|^ignored_|^unused_ 497 | 498 | # Tells whether we should check for unused import in __init__ files. 499 | init-import=no 500 | 501 | # List of qualified module names which can have objects that can redefine 502 | # builtins. 503 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 504 | 505 | 506 | [CLASSES] 507 | 508 | # Warn about protected attribute access inside special methods 509 | check-protected-access-in-special-methods=no 510 | 511 | # List of method names used to declare (i.e. assign) instance attributes. 512 | defining-attr-methods=__init__, 513 | __new__, 514 | setUp, 515 | __post_init__ 516 | 517 | # List of member names, which should be excluded from the protected access 518 | # warning. 519 | exclude-protected=_asdict, 520 | _fields, 521 | _replace, 522 | _source, 523 | _make 524 | 525 | # List of valid names for the first argument in a class method. 526 | valid-classmethod-first-arg=cls 527 | 528 | # List of valid names for the first argument in a metaclass class method. 529 | valid-metaclass-classmethod-first-arg=cls 530 | 531 | 532 | [DESIGN] 533 | 534 | # Maximum number of arguments for function / method. 535 | max-args=5 536 | 537 | # Maximum number of attributes for a class (see R0902). 538 | max-attributes=7 539 | 540 | # Maximum number of boolean expressions in an if statement (see R0916). 541 | max-bool-expr=5 542 | 543 | # Maximum number of branch for function / method body. 544 | max-branches=12 545 | 546 | # Maximum number of locals for function / method body. 547 | max-locals=15 548 | 549 | # Maximum number of parents for a class (see R0901). 550 | max-parents=7 551 | 552 | # Maximum number of public methods for a class (see R0904). 553 | max-public-methods=20 554 | 555 | # Maximum number of return / yield for function / method body. 556 | max-returns=6 557 | 558 | # Maximum number of statements in function / method body. 559 | max-statements=50 560 | 561 | # Minimum number of public methods for a class (see R0903). 562 | min-public-methods=2 563 | 564 | 565 | [IMPORTS] 566 | 567 | # List of modules that can be imported at any level, not just the top level 568 | # one. 569 | allow-any-import-level= 570 | 571 | # Allow wildcard imports from modules that define __all__. 572 | allow-wildcard-with-all=no 573 | 574 | # Analyse import fallback blocks. This can be used to support both Python 2 and 575 | # 3 compatible code, which means that the block might have code that exists 576 | # only in one or another interpreter, leading to false positives when analysed. 577 | analyse-fallback-blocks=no 578 | 579 | # Deprecated modules which should not be used, separated by a comma. 580 | deprecated-modules=optparse,tkinter.tix 581 | 582 | # Output a graph (.gv or any supported image format) of external dependencies 583 | # to the given file (report RP0402 must not be disabled). 584 | ext-import-graph= 585 | 586 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 587 | # external) dependencies to the given file (report RP0402 must not be 588 | # disabled). 589 | import-graph= 590 | 591 | # Output a graph (.gv or any supported image format) of internal dependencies 592 | # to the given file (report RP0402 must not be disabled). 593 | int-import-graph= 594 | 595 | # Force import order to recognize a module as part of the standard 596 | # compatibility libraries. 597 | known-standard-library= 598 | 599 | # Force import order to recognize a module as part of a third party library. 600 | known-third-party=enchant 601 | 602 | # Couples of modules and preferred modules, separated by a comma. 603 | preferred-modules= 604 | 605 | 606 | [EXCEPTIONS] 607 | 608 | # Exceptions that will emit a warning when being caught. Defaults to 609 | # "BaseException, Exception". 610 | overgeneral-exceptions=BaseException, 611 | Exception 612 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = py-to-ts-interfaces 3 | version = 1.4.0 4 | author = Peter Bell 5 | author_email = syndallic@outlook.com 6 | description = A script to generate TypeScript interfaces from Python dataclasses, with enum support. 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/Syndallic/py-to-ts-interfaces 10 | project_urls = 11 | Bug Tracker = https://github.com/Syndallic/py-to-ts-interfaces/issues 12 | classifiers = 13 | Programming Language :: Python :: 3 14 | License :: OSI Approved :: MIT License 15 | Operating System :: OS Independent 16 | 17 | [options] 18 | packages = py_to_ts_interfaces 19 | python_requires = >=3.9 20 | 21 | [options.entry_points] 22 | console_scripts = 23 | py-to-ts-interfaces = py_to_ts_interfaces.__main__:main -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Syndallic/py-to-ts-interfaces/45d9dfb47252bf399951e4e7e71514bfdb8586c5/tests/__init__.py -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from py_to_ts_interfaces.__main__ import python_to_typescript_folder 5 | 6 | PYTHON_DEFINITIONS = """from dataclasses import dataclass 7 | from enum import Enum 8 | from typing import Final, Union, List, Dict 9 | 10 | class MyEnum(Enum): 11 | FIRST = "Number One" 12 | SECOND = "Number Two" 13 | 14 | def __str__(self): 15 | return self.value 16 | 17 | CONSTANT_STRING: Final = "example" 18 | OTHER_STRING = "another example" 19 | 20 | @dataclass(frozen=True) 21 | class MyInterface: 22 | field: MyEnum 23 | otherField: List[MyEnum] 24 | 25 | @dataclass(frozen=True) 26 | class MyNullableInterface: 27 | field: Union[MyInterface, None] = None 28 | otherField: Optional[MyInterface] = None 29 | 30 | @dataclass(frozen=True) 31 | class MyInterface2: 32 | strange_type: Optional[List[int]] 33 | other_type: List[str] 34 | dict_type: Dict[int, Dict[str, MyEnum]] 35 | 36 | """ 37 | 38 | TYPESCRIPT_DEFINITIONS = """export enum MyEnum { 39 | FIRST = 'Number One', 40 | SECOND = 'Number Two', 41 | } 42 | 43 | export const CONSTANT_STRING = 'example'; 44 | export const OTHER_STRING = 'another example'; 45 | 46 | export interface MyInterface { 47 | field: MyEnum; 48 | otherField: MyEnum[]; 49 | } 50 | 51 | export interface MyNullableInterface { 52 | field?: MyInterface; 53 | otherField?: MyInterface; 54 | } 55 | 56 | export interface MyInterface2 { 57 | strangeType?: number[]; 58 | otherType: string[]; 59 | dictType: Record>; 60 | } 61 | """ 62 | 63 | 64 | # pylint: disable=no-self-use 65 | class TestPyToTsInterfaces: 66 | """Tests for this module""" 67 | 68 | def test_success(self): 69 | """Primitive 'catch-all' test.""" 70 | 71 | input_path = "temp_testing" 72 | output_path = "temp_testing_2" 73 | 74 | if not os.path.isdir(input_path): 75 | os.makedirs(input_path) 76 | with open(os.path.join(input_path, "temp.py"), "w+", encoding="utf-8") as writer: 77 | writer.write(PYTHON_DEFINITIONS) 78 | 79 | python_to_typescript_folder(input_path, output_path) 80 | 81 | with open(os.path.join(output_path, "temp.ts"), "r", encoding="utf-8") as reader: 82 | output = reader.read() 83 | 84 | shutil.rmtree(input_path) 85 | shutil.rmtree(output_path) 86 | 87 | output_split = output.split("\n") 88 | expected_split = TYPESCRIPT_DEFINITIONS.split("\n") 89 | print("\n\nOutput:\n\"{}\"".format(output)) 90 | 91 | for i in range(max(len(output_split), len(expected_split))): 92 | assert output_split[i] == expected_split[i] 93 | --------------------------------------------------------------------------------