├── scratch ├── test.py ├── bug15.ts ├── coroutine_type.py ├── DeferredEvaluationTest.py ├── snippet.ts ├── promiselike_test.py └── typeddict_test.py ├── screenshot.png ├── setup.cfg ├── tests_grammar ├── Types.ini ├── 02_test_Keywords.ini ├── 01_test_Entities.ini ├── 15_test_compatibility_3_9.ini ├── 14_test_compatibility_3_8.ini ├── 09_test_Imports.ini ├── 04_test_Consts.ini ├── 20_test_compatibility_3_14.ini ├── 16_test_compatibility_3_10.ini ├── 13_test_compatibility_3_7.ini ├── 03_test_literals.ini ├── 07_test_Types.ini ├── 19_test_compatibility_3_13.ini ├── 17_test_compatibility_3_11.ini ├── 18_test_compatibility_3_12.ini ├── Playground.ini ├── 05_test_Enums.ini ├── 10_test_Typescript_Document.ini ├── 06_test_Namespaces.ini ├── 11_test_comments.ini └── 12_test_anonymous_toplevel.ini ├── .idea ├── vcs.xml ├── inspectionProfiles │ └── profiles_settings.xml ├── modules.xml ├── misc.xml └── ts2python.iml ├── demo ├── demo.bat ├── demo-3-16.bat ├── demo-3-16.sh ├── demo.sh ├── patch_vscode_d.py ├── test.sh ├── test2.sh ├── README_example.py └── extract_ts_from_lsp_specs.py ├── tests ├── run.sh ├── test_singledispatch_shim.py ├── test_ts2python_fwrefs.py └── runner.py ├── docs ├── Makefile ├── make.bat ├── conf.py ├── BasicUsage.rst ├── Validation.rst └── index.rst ├── .readthedocs.yaml ├── ts2pythonConfig.ini ├── ts2python ├── __init__.py ├── singledispatch_shim.py └── typeddict_shim.py ├── pyproject.toml ├── .gitignore ├── Changes.txt ├── tst_ts2python_grammar.py ├── ts2python.ebnf ├── README.md └── LICENSE /scratch/test.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jecki/ts2python/HEAD/screenshot.png -------------------------------------------------------------------------------- /scratch/bug15.ts: -------------------------------------------------------------------------------- 1 | export interface ServiceHost { url: string, categories: Array, } 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_files = LICENSE 3 | 4 | [bdist_wheel] 5 | universal = 0 6 | 7 | [pep8] 8 | max-line-length = 100 9 | ignore = E303, E731 10 | -------------------------------------------------------------------------------- /tests_grammar/Types.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.RenderAnonymous = "toplevel" 3 | 4 | [match:document] 5 | M3: "interface test { 6 | s: string | { language: string; value: string };}" 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests_grammar/02_test_Keywords.ini: -------------------------------------------------------------------------------- 1 | 2 | [match:basic_type] 3 | 4 | [ast:basic_type] 5 | 6 | [fail:basic_type] 7 | 8 | 9 | [match:array_marker] 10 | 11 | [ast:array_marker] 12 | 13 | [fail:array_marker] 14 | 15 | -------------------------------------------------------------------------------- /scratch/coroutine_type.py: -------------------------------------------------------------------------------- 1 | # from typing import Coroutine 2 | from collections.abc import Coroutine 3 | # from typing_extensions import Coroutine 4 | 5 | class TextDocument: 6 | 7 | def save(self) -> Coroutine[bool]: 8 | pass 9 | 10 | -------------------------------------------------------------------------------- /tests_grammar/01_test_Entities.ini: -------------------------------------------------------------------------------- 1 | [match:variable] 2 | M1: """textDocument.codeAction.resolveSupport""" 3 | 4 | [match:identifier] 5 | 6 | [ast:identifier] 7 | 8 | [fail:identifier] 9 | 10 | 11 | [match:EOF] 12 | 13 | [ast:EOF] 14 | 15 | [fail:EOF] 16 | 17 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/demo.bat: -------------------------------------------------------------------------------- 1 | echo "ts2python demo: Converts the current specifications of the lanugage server protocol to python" 2 | python extract_ts_from_lsp_specs.py https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/3.17/specification.md 3 | python ../ts2pythonParser.py specification.ts 4 | echo "Now have a look at specification.py :-)" 5 | -------------------------------------------------------------------------------- /demo/demo-3-16.bat: -------------------------------------------------------------------------------- 1 | echo "ts2python demo: Converts the current specifications of the lanugage server protocol to python" 2 | python extract_ts_from_lsp_specs.py https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/specification-3-16.md 3 | python ../ts2pythonParser.py specification-3-16.ts 4 | echo "Now have a look at specification.py :-)" 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /demo/demo-3-16.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "ts2python demo: Converts the current specifications of the lanugage server protocol to python" 4 | python extract_ts_from_lsp_specs.py https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/specification-3-16.md 5 | python ../ts2pythonParser.py specification-3-16.ts 6 | echo "Checking specification-3-16.py..." 7 | python specification-3-16.py 8 | echo "Now have a look at specification-3-16.py :-)" 9 | -------------------------------------------------------------------------------- /demo/demo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "ts2python demo: Converts the current specifications of the lanugage server protocol to python" 4 | python3 extract_ts_from_lsp_specs.py https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/3.18/specification.md 5 | python3 ../ts2pythonParser.py specification.ts 6 | echo "Checking specification.py..." 7 | export PYTHONPATH=../:$PYTHONPATH 8 | python3 specification.py 9 | echo "Now have a look at specification.py :-)" 10 | -------------------------------------------------------------------------------- /tests_grammar/15_test_compatibility_3_9.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.RenderAnonymous = "local" 3 | ts2python.UseTypeUnion = False 4 | ts2python.UsePostponedEvaluation = False 5 | ts2python.UseExplicitTypeAlias = False 6 | ts2python.UseTypeParameters = False 7 | ts2python.UseLiteralType = True 8 | ts2python.UseVariadicGenerics = False 9 | ts2python.UseNotRequired = False 10 | ts2python.AllowReadOnly = False 11 | ts2python.AssumeDeferredEvaluation = False 12 | 13 | 14 | [match:root] 15 | M1: "" 16 | -------------------------------------------------------------------------------- /tests/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | 3 | echo "Running unit-tests of ts2python" 4 | 5 | python3.11 runner.py 6 | python3.10 runner.py 7 | python3.9 runner.py 8 | python3.8 runner.py 9 | python3.7 runner.py 10 | python3.6 runner.py 11 | pypy3 runner.py 12 | 13 | echo "Running grammar tests of ts2python" 14 | 15 | python3.11 ../tst_ts2python_grammar.py 16 | python3.10 ../tst_ts2python_grammar.py 17 | python3.9 ../tst_ts2python_grammar.py 18 | python3.8 ../tst_ts2python_grammar.py 19 | python3.7 ../tst_ts2python_grammar.py 20 | python3.6 ../tst_ts2python_grammar.py 21 | pypy3 ../tst_ts2python_grammar.py 22 | -------------------------------------------------------------------------------- /scratch/DeferredEvaluationTest.py: -------------------------------------------------------------------------------- 1 | # from __future__ import annotations 2 | 3 | 4 | import sys 5 | from enum import Enum, IntEnum 6 | 7 | from typing import Union, Optional, Any, Generic, TypeVar, Callable, List, \ 8 | Iterable, Iterator, Tuple, Dict, TypedDict, NotRequired, ReadOnly, Literal 9 | from collections.abc import Coroutine 10 | 11 | class SemanticTokensDelta(TypedDict): 12 | resultId: NotRequired[ReadOnly[str]] 13 | edits: List[SemanticTokensEdit] 14 | 15 | 16 | class SemanticTokensEdit(TypedDict): 17 | start: int 18 | deleteCount: int 19 | data: NotRequired[List[int]] 20 | -------------------------------------------------------------------------------- /.idea/ts2python.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tests_grammar/14_test_compatibility_3_8.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.RenderAnonymous = "local" 3 | ts2python.UsePostponedEvaluation = False 4 | ts2python.UseTypeUnion = False 5 | ts2python.UseExplicitTypeAlias = False 6 | ts2python.UseTypeParameters = False 7 | ts2python.UseLiteralType = True 8 | ts2python.UseVariadicGenerics = False 9 | ts2python.UseNotRequired = False 10 | ts2python.AllowReadOnly = False 11 | ts2python.AssumeDeferredEvaluation = False 12 | 13 | 14 | [match:types] 15 | M1: "true" 16 | 17 | [py:types] 18 | M1: "Literal[True]" 19 | 20 | [match:type_alias] 21 | M1: """export type LSPAny = LSPObject | LSPArray | string | integer;""" 22 | 23 | [py:type_alias] 24 | M1: """LSPAny = Union['LSPObject', 'LSPArray', str, int]""" 25 | 26 | [match:root] 27 | M1: "" 28 | -------------------------------------------------------------------------------- /scratch/snippet.ts: -------------------------------------------------------------------------------- 1 | // Just for testing 2 | 3 | interface CalendarJSON { 4 | date: string; 5 | fiscal: { 6 | month: MonthNumbers; 7 | quarter: 8 | | { name: 'Q1'; value: 1 } 9 | | { name: 'Q2'; value: 2 } 10 | | { name: 'Q3'; value: 3 } 11 | | { name:; 'Q4'; value: 4 }; 12 | week: WeekNumbers; 13 | year: CommonYears; 14 | }; 15 | gregorian: { 16 | day_of_week: 17 | | { name: 'Monday'; value: 0 } 18 | | { name: 'Tuesday'; value: 1 } 19 | | { name: 'Wednesday'; value: 2 } 20 | | { name: 'Thursday'; value: 3 } 21 | | { name: 'Friday'; value: 4 } 22 | | { name: 'Saturday'; value: 5 } 23 | | { name: 'Sunday'; value: 6 }; 24 | month: MonthNumbers; 25 | quarter: QuarterNumbers; 26 | week: WeekNumbers; 27 | year: CommonYears; 28 | }; 29 | id: string; 30 | } 31 | -------------------------------------------------------------------------------- /tests_grammar/09_test_Imports.ini: -------------------------------------------------------------------------------- 1 | [match:symbol] 2 | M1: "pi as π" 3 | 4 | [match:Import] 5 | M1: 'import helloWorld from "./hello.js"' 6 | M2: 'import { pi, phi, absolute } from "./maths.js"' 7 | M3: 'import { pi as π } from "./maths.js"' 8 | M4: 'import RandomNumberGenerator, { pi as π } from "./maths.js"' 9 | M5: 'import * as math from "./maths.js"' 10 | M6: 'import "./maths.js"' 11 | M7: 'import { createCatName, type Cat, type Dog } from "./animal.js"' 12 | 13 | [match:document] 14 | M1: """import {ChangeInfo, CommentRange} from './rest-api'; 15 | export declare interface ChecksPluginApi { 16 | register(provider: ChecksProvider, config?: ChecksApiConfig): void; 17 | announceUpdate(): void; 18 | updateResult(run: CheckRun, result: CheckResult): void; 19 | }""" 20 | 21 | M2: """import { A } from '../interfaces.ts' 22 | 23 | export interface B extends A { 24 | }""" 25 | -------------------------------------------------------------------------------- /demo/patch_vscode_d.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Adds an empty PromiseLike-dummy class to vscode.d.py""" 4 | 5 | import sys 6 | 7 | def patch_vscode_d_py(): 8 | with open("vscode.d.py", 'r', encoding='utf-8') as f: 9 | vscode_d = f.read() 10 | i = vscode_d.find("import sys") 11 | i = vscode_d.find("\n", i) + 1 12 | vscode_d = ''.join([vscode_d[:i], "import os\nsys.path.append(os.path.abspath('..'))\n", vscode_d[i:]]) 13 | # i = vscode_d.find('PromiseLike') 14 | # i = vscode_d.rfind('\n', 0, i) 15 | # vscode_d = ''.join([vscode_d[:i], 16 | # "\n\nPromiseLike = List # just a hack, won't appear in production code ;-) \n", 17 | # vscode_d[i:]]) 18 | with open("vscode.d.py", 'w', encoding='utf-8') as f: 19 | f.write(vscode_d) 20 | 21 | if __name__ == '__main__': 22 | patch_vscode_d_py() 23 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Formats, htmlzip, pdf, epub 9 | formats: 10 | - htmlzip 11 | 12 | # Set the version of Python and other tools you might need 13 | build: 14 | os: ubuntu-22.04 15 | tools: 16 | python: "3.10" 17 | # You can also specify other tool versions: 18 | # nodejs: "16" 19 | # rust: "1.55" 20 | # golang: "1.17" 21 | 22 | # Build documentation in the docs/ directory with Sphinx 23 | sphinx: 24 | configuration: docs/conf.py 25 | 26 | # Optionally build your docs in additional formats such as PDF 27 | # formats: 28 | # - pdf 29 | 30 | # Optionally declare the Python requirements required to build your docs 31 | # python: 32 | # install: 33 | # - requirements: documentation_src/requirements.txt 34 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /ts2pythonConfig.ini: -------------------------------------------------------------------------------- 1 | [ts2python] 2 | RenderAnonymous = 'local' # rendering of anonymous TypedDicts: "type", "functional", "local", "toplevel" 3 | UseEnum = True # PEP 435, Python 3.4 4 | UsePostponedEvaluation = False # PEP 563, Python 3.7 (adds "from __future__ import annotations") 5 | UseLiteralType = False # PEP 584, 586, Python 3.8 6 | UseTypeUnion = False # PEP 604, Python 3.10 7 | UseExplicitTypeAlias = False # PEP 613, Python 3.10 8 | UseVariadicGenerics = False # PEP 646, Python 3.11 9 | UseNotRequired = True # PEP 655, Python 3.11 10 | UseTypeParameters = False # PEP 695, Python 3.12 11 | AllowReadOnly = True # PEP 705, Python 3.13 12 | AssumeDeferredEvaluation = False # PEP 649, 749, Python 3.14 13 | KeepMultilineComments = True # keep comments in AST 14 | 15 | [DHParser] 16 | # batch_processing_parallelization = False # use this for debugging 17 | 18 | -------------------------------------------------------------------------------- /tests_grammar/04_test_Consts.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.UseNotRequired = True 3 | ts2python.UsePostponedEvaluation = False 4 | 5 | [match:const] 6 | M1: """export const jsonrpcReservedErrorRangeStart: integer = -32099;""" 7 | M2: """export const serverErrorStart: integer = jsonrpcReservedErrorRangeStart;""" 8 | M3: """export const jsonrpcReservedErrorRangeEnd = -32000;""" 9 | M4: """export const EOL: string[] = ['\n', '\r\n', '\r'];""" 10 | M5: """export const version: string;""" 11 | 12 | [ast:const] 13 | 14 | [fail:const] 15 | 16 | [match:assignment] 17 | M1: """textDocument.codeAction.resolveSupport = { properties: ['edit'] };""" 18 | 19 | [match:declaration] 20 | M1: "zahl: integer" 21 | M2: "hoverMessage?: MarkdownString | MarkedString | Array" 22 | 23 | [ast:declaration] 24 | M1: (declaration (qualifiers) (identifier "zahl") (types (type (basic_type "integer")))) 25 | 26 | [py:declaration] 27 | M2: "hoverMessage: NotRequired[Union['MarkdownString', 'MarkedString', List[Union['MarkdownString', 'MarkedString']]]]" 28 | -------------------------------------------------------------------------------- /tests_grammar/20_test_compatibility_3_14.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.RenderAnonymous = "local" 3 | ts2python.UsePostponedEvaluation = False 4 | ts2python.UseTypeUnion = True 5 | ts2python.UseExplicitTypeAlias = False 6 | ts2python.UseTypeParameters = True 7 | ts2python.UseLiteralType = True 8 | ts2python.UseVariadicGenerics = True 9 | ts2python.UseNotRequired = True 10 | ts2python.AllowReadOnly = True 11 | ts2python.AssumeDeferredEvaluation = True 12 | 13 | 14 | [match:root] 15 | M1: "" 16 | 17 | 18 | [match:document] 19 | M1: """export interface SemanticTokensDelta { 20 | readonly resultId?: string; 21 | edits: SemanticTokensEdit[]; 22 | } 23 | 24 | export interface SemanticTokensEdit { 25 | start: uinteger; 26 | deleteCount: uinteger; 27 | data?: uinteger[]; 28 | }""" 29 | M2: """export type IconPath = Uri | { 30 | light: Uri; 31 | dark: Uri; 32 | } | ThemeIcon;""" 33 | 34 | [py:document] 35 | M2: """ 36 | class IconPath_1(TypedDict): 37 | light: Uri 38 | dark: Uri 39 | type IconPath = Uri | IconPath_1 | ThemeIcon""" -------------------------------------------------------------------------------- /ts2python/__init__.py: -------------------------------------------------------------------------------- 1 | """__init__.py - package definition module for ts2python 2 | 3 | Copyright 2021 by Eckhart Arnold (arnold@badw.de) 4 | Bavarian Academy of Sciences an Humanities (badw.de) 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 15 | implied. See the License for the specific language governing 16 | permissions and limitations under the License. 17 | """ 18 | 19 | import sys 20 | assert sys.version_info >= (3, 7, 0), "ts2python requires at least Python-Version 3.7!" 21 | 22 | __title__ = "ts2python" 23 | __version__ = "0.8.1" 24 | __version_info__ = tuple(int(part) for part in __version__.split('.')) 25 | __description__ = "Python-Interoperability for Typescript-Interfaces" 26 | __author__ = "Eckhart Arnold" 27 | __email__ = "eckhart.arnold@posteo.de" 28 | __license__ = "http://www.apache.org/licenses/LICENSE-2.0" 29 | __copyright__ = "Copyright (C) Eckhart Arnold 2021" 30 | 31 | -------------------------------------------------------------------------------- /tests_grammar/16_test_compatibility_3_10.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.RenderAnonymous = "local" 3 | ts2python.UsePostponedEvaluation = True 4 | ts2python.UseTypeUnion = True 5 | ts2python.UseExplicitTypeAlias = True 6 | ts2python.UseTypeParameters = False 7 | ts2python.UseLiteralType = True 8 | ts2python.UseVariadicGenerics = False 9 | ts2python.UseNotRequired = False 10 | ts2python.AllowReadOnly = False 11 | ts2python.AssumeDeferredEvaluation = False 12 | 13 | 14 | [match:interface] 15 | M1: """interface OptionalValue { 16 | option?: Alpha | string; 17 | }""" 18 | M2: """interface ProgressParams { 19 | token: ProgressToken; 20 | value: T; 21 | }""" 22 | 23 | [py:interface] 24 | M1: """class OptionalValue(TypedDict, total=False): 25 | option: NotRequired[Alpha | str]""" 26 | M2: """T = TypeVar('T') 27 | 28 | class ProgressParams(Generic[T], GenericTypedDict): 29 | token: ProgressToken 30 | value: T""" 31 | 32 | 33 | [match:document] 34 | M1: """export type IconPath = Uri | { 35 | light: Uri; 36 | dark: Uri; 37 | } | ThemeIcon;""" 38 | 39 | [py:document] 40 | M1: """ 41 | class IconPath_1(TypedDict): 42 | light: Uri 43 | dark: Uri 44 | IconPath: TypeAlias = 'Uri | IconPath_1 | ThemeIcon'""" 45 | 46 | 47 | [match:root] 48 | M1: "" 49 | -------------------------------------------------------------------------------- /tests_grammar/13_test_compatibility_3_7.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.RenderAnonymous = "local" 3 | ts2python.UsePostponedEvaluation = False 4 | ts2python.UseTypeUnion = False 5 | ts2python.UseExplicitTypeAlias = False 6 | ts2python.UseTypeParameters = False 7 | ts2python.UseLiteralType = False 8 | ts2python.UseVariadicGenerics = False 9 | ts2python.UseNotRequired = False 10 | ts2python.AllowReadOnly = False 11 | ts2python.AssumeDeferredEvaluation = False 12 | 13 | 14 | [match:document] 15 | M1: """export interface TextDocument { 16 | readonly additionalCommonProperties?: Record; 17 | save(): Thenable; 18 | } 19 | 20 | 21 | interface Thenable extends PromiseLike { } 22 | 23 | export type DocumentSelector = DocumentFilter | string | ReadonlyArray;""" 24 | 25 | [py:document] 26 | M1: """class TextDocument: 27 | additionalCommonProperties: Optional[Dict[str, Any]] 28 | 29 | def save(self) -> 'Thenable[bool]': 30 | pass 31 | 32 | T = TypeVar('T') 33 | 34 | PromiseLike = Iterable # Admittedly, a very poor hack 35 | 36 | class Thenable(PromiseLike[T], Generic[T]): 37 | pass 38 | 39 | DocumentSelector = Union['DocumentFilter', str, List[Union['DocumentFilter', str]]]""" 40 | 41 | 42 | [match:root] 43 | M1: "" 44 | -------------------------------------------------------------------------------- /demo/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export PYTHONPATH=../:$PYTHONPATH 4 | 5 | 6 | echo "Testing python3.14 compatibility" 7 | python3.14 ../ts2pythonParser.py --compatibility 3.14 specification.ts 8 | python3.14 specification.py 9 | 10 | echo "Testing python3.13 compatibility" 11 | python3.13 ../ts2pythonParser.py --compatibility 3.13 specification.ts 12 | python3.13 specification.py 13 | 14 | echo "Testing python3.12 compatibility" 15 | python3.12 ../ts2pythonParser.py --compatibility 3.12 specification.ts 16 | python3.12 specification.py 17 | 18 | echo "Testing python3.11 compatibility" 19 | python3.11 ../ts2pythonParser.py --compatibility 3.11 specification.ts 20 | python3.11 specification.py 21 | 22 | echo "Testing python3.10 compatibility" 23 | python3.10 ../ts2pythonParser.py --compatibility 3.10 specification.ts 24 | python3.10 specification.py 25 | 26 | echo "Testing pypy3 (3.10) compatibility" 27 | pypy3 ../ts2pythonParser.py --compatibility 3.10 specification.ts 28 | pypy3 specification.py 29 | 30 | echo "Testing python3.9 compatibility" 31 | python3.9 ../ts2pythonParser.py --compatibility 3.9 specification.ts 32 | python3.9 specification.py 33 | 34 | echo "Testing python3.8 compatibility" 35 | python3.8 ../ts2pythonParser.py --compatibility 3.8 specification.ts 36 | python3.8 specification.py 37 | 38 | echo "Testing python3.7 compatibility" 39 | python3.8 ../ts2pythonParser.py --compatibility 3.7 specification.ts 40 | python3.7 specification.py 41 | -------------------------------------------------------------------------------- /demo/test2.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export PYTHONPATH=../:$PYTHONPATH 4 | 5 | 6 | echo "Testing python3.14 compatibility" 7 | python3.14 ../ts2pythonParser.py --compatibility 3.14 vscode.d.ts 8 | python3.14 patch_vscode_d.py 9 | python3.14 vscode.d.py 10 | 11 | echo "Testing python3.13 compatibility" 12 | python3.13 ../ts2pythonParser.py --compatibility 3.13 vscode.d.ts 13 | python3.13 patch_vscode_d.py 14 | python3.13 vscode.d.py 15 | 16 | echo "Testing python3.12 compatibility" 17 | python3.12 ../ts2pythonParser.py --compatibility 3.12 vscode.d.ts 18 | python3.12 patch_vscode_d.py 19 | python3.12 vscode.d.py 20 | 21 | echo "Testing python3.11 compatibility" 22 | python3.11 ../ts2pythonParser.py --compatibility 3.11 vscode.d.ts 23 | python3.11 patch_vscode_d.py 24 | python3.11 vscode.d.py 25 | 26 | echo "Testing python3.10 compatibility" 27 | python3.10 ../ts2pythonParser.py --compatibility 3.10 vscode.d.ts 28 | python3.10 patch_vscode_d.py 29 | python3.10 vscode.d.py 30 | 31 | echo "Testing pypy3 (3.10) compatibility" 32 | pypy3 ../ts2pythonParser.py --compatibility 3.10 vscode.d.ts 33 | pypy3 patch_vscode_d.py 34 | pypy3 vscode.d.py 35 | 36 | echo "Testing python3.9 compatibility" 37 | python3.9 ../ts2pythonParser.py --compatibility 3.9 vscode.d.ts 38 | python3.9 patch_vscode_d.py 39 | python3.9 vscode.d.py 40 | 41 | echo "Testing python3.8 compatibility" 42 | python3.8 ../ts2pythonParser.py --compatibility 3.8 vscode.d.ts 43 | python3.8 patch_vscode_d.py 44 | python3.8 vscode.d.py 45 | 46 | echo "Testing python3.7 compatibility" 47 | python3.8 ../ts2pythonParser.py --compatibility 3.7 vscode.d.ts 48 | python3.8 patch_vscode_d.py 49 | python3.7 vscode.d.py 50 | -------------------------------------------------------------------------------- /tests_grammar/03_test_literals.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.RenderAnonymous = "local" 3 | ts2python.UsePostponedEvaluation = True 4 | ts2python.UseLiteralType = True 5 | ts2python.AssumeDeferredEvaluation = False 6 | 7 | [match:document] 8 | M1: """export type FailureHandlingKind = "abort" | "transactional" | "undo" 9 | | "textOnlyTransactional";""" 10 | 11 | [py:document] 12 | M1: '''FailureHandlingKind = Literal["abort", "transactional", "undo", "textOnlyTransactional"]''' 13 | 14 | 15 | [match:_array_ellipsis] 16 | M1: '''{ line: 2, startChar: 5, length: 3, tokenType: "property", 17 | tokenModifiers: ["private", "static"] 18 | }, 19 | { line: 2, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }, 20 | { line: 5, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] }''' 21 | 22 | [match:literal] 23 | M1: "{ properties: ['edit'] }" 24 | 25 | [fail:literal] 26 | F1: "{ name: 'Q1'; value: 1 }" 27 | 28 | 29 | [ast:literal] 30 | 31 | [fail:literal] 32 | 33 | 34 | [match:number] 35 | M1: "-32700" 36 | 37 | [ast:number] 38 | 39 | [fail:number] 40 | 41 | 42 | [match:string] 43 | 44 | [ast:string] 45 | 46 | [fail:string] 47 | 48 | 49 | [match:array] 50 | 51 | [ast:array] 52 | 53 | [fail:array] 54 | 55 | [match:object] 56 | M1: '''{ 57 | "title": "Do Foo" 58 | }''' 59 | M2: '''{ line: 2, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }''' 60 | 61 | 62 | [match:type_alias] 63 | M2: "export type LiteralTest = 'left' | 'right';" 64 | M3: "export type LayoutPosition = 'left' | 'top' | 'right' | 'bottom' | 'center' | 'chartArea' | {[scaleId: string]: number};" 65 | -------------------------------------------------------------------------------- /demo/README_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | try: 4 | from ts2python.json_validation import TypedDict, type_check 5 | except ImportError: 6 | # It seems that this script has been called from the git 7 | # repository without "ts2python" having been installed 8 | import sys, os 9 | sys.path.append(os.path.abspath('..')) 10 | from ts2python.json_validation import TypedDict, type_check 11 | 12 | 13 | class Position(TypedDict, total=True): 14 | line: int 15 | character: int 16 | 17 | 18 | class Range(TypedDict, total=True): 19 | start: Position 20 | end: Position 21 | 22 | 23 | @type_check 24 | def middle_line(rng: Range) -> Position: 25 | line = (rng['start']['line'] + rng['end']['line']) // 2 26 | character = 0 27 | return Position(line=line, character=character) 28 | 29 | 30 | rng = {'start': {'line': 1, 'character': 1}, 31 | 'end': {'line': 8, 'character': 17}} 32 | 33 | assert middle_line(rng) == {'line': 4, 'character': 0} 34 | 35 | expected_error = """Parameter "rng" of function "middle_line" failed the type-check, because: 36 | Type error(s) in dictionary of type : 37 | Field start: '1' is not of , but of type 38 | Field end: '8' is not of , but of type """ 39 | 40 | malformed_rng = {'start': 1, 'end': 8} 41 | try: 42 | middle_line(malformed_rng) 43 | print("At this point a type error was expected, but did not occur!") 44 | sys.exit(1) 45 | except TypeError as e: 46 | if str(e) != expected_error: 47 | print("A different error than the expected one occurred!") 48 | sys.exit(1) 49 | else: 50 | print("@type_check-decorator test successful.") 51 | 52 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "ts2python" 3 | version = "0.8.2" 4 | description = "Python-Interoperability for Typescript-Interfaces" 5 | 6 | license = "Apache-2.0" 7 | 8 | authors = ["Eckhart Arnold "] 9 | 10 | readme = "README.md" 11 | repository = "https://github.com/jecki/ts2python" 12 | homepage = "https://github.com/jecki/ts2python" 13 | 14 | keywords = [ 15 | 'Typescript to Python converter', 16 | 'Typescript Interface', 17 | 'Python TypedDict', 18 | ] 19 | 20 | classifiers = [ 21 | 'Development Status :: 5 - Production/Stable', 22 | 'Intended Audience :: Developers', 23 | 'Environment :: Console', 24 | 'License :: OSI Approved :: Apache Software License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3.8', 28 | 'Programming Language :: Python :: 3.9', 29 | 'Programming Language :: Python :: 3.10', 30 | 'Programming Language :: Python :: 3.11', 31 | 'Programming Language :: Python :: 3.12', 32 | 'Programming Language :: Python :: 3.13', 33 | 'Programming Language :: Python :: 3.14', 34 | 'Programming Language :: Python :: Implementation :: CPython', 35 | 'Programming Language :: Python :: Implementation :: PyPy', 36 | 'Topic :: Text Processing :: Markup', 37 | 'Topic :: Software Development :: Code Generators', 38 | 'Topic :: Software Development :: Compilers' 39 | ] 40 | 41 | packages = [ 42 | { include = "ts2pythonParser.py" }, 43 | { include = "ts2pythonExplorer.py" }, 44 | { include = "ts2pythonConfig.ini" }, 45 | { include = "ts2python.ebnf" }, 46 | { include = "ts2python" } 47 | ] 48 | 49 | [tool.poetry.dependencies] 50 | python = "^3.8" 51 | DHParser = { version = "^1.9.1", optional = false } 52 | typing_extensions = { version = "^4.0", optional = true } 53 | 54 | [tool.poetry.scripts] 55 | ts2python = 'ts2pythonParser:main' 56 | ts2pythonExplorer = 'ts2pythonExplorer:main' 57 | 58 | [build-system] 59 | requires = ["poetry-core>=1.0.0", "setuptools", "wheel"] 60 | build-backend = "poetry.core.masonry.api" 61 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | modules_path = os.path.abspath('../ts2python') 17 | if modules_path not in sys.path: 18 | sys.path.append(modules_path) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'ts2python' 24 | copyright = '2021, Eckhart Arnold' 25 | author = 'Eckhart Arnold' 26 | 27 | # The full version, including alpha/beta/rc tags 28 | release = '0.5' 29 | 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 'sphinx.ext.autodoc' 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 46 | 47 | 48 | # -- Options for HTML output ------------------------------------------------- 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | html_theme = 'alabaster' 54 | 55 | # Add any paths that contain custom static files (such as style sheets) here, 56 | # relative to this directory. They are copied after the builtin static files, 57 | # so a file named "default.css" will overwrite the builtin "default.css". 58 | html_static_path = ['_static'] -------------------------------------------------------------------------------- /scratch/promiselike_test.py: -------------------------------------------------------------------------------- 1 | # Generated by ts2python version 0.8.0 on 2025-06-16 10:58:11.823296 2 | # compatibility level: Python 3.7 and above 3 | 4 | 5 | from __future__ import annotations 6 | 7 | 8 | import sys 9 | from enum import Enum, IntEnum 10 | 11 | from typing import Union, Optional, Any, Generic, TypeVar, Callable, List, \ 12 | Iterable, Iterator, Tuple, Dict, Literal, Awaitable 13 | 14 | 15 | try: 16 | from ts2python.typeddict_shim import TypedDict, GenericTypedDict, NotRequired, Literal, \ 17 | ReadOnly, TypeAlias 18 | # Override typing.TypedDict for Runtime-Validation 19 | except ImportError: 20 | print("Module ts2python.typeddict_shim not found. Only coarse-grained " 21 | "runtime type-validation of TypedDicts possible") 22 | try: 23 | from typing import TypedDict, Literal 24 | except ImportError: 25 | try: 26 | from ts2python.typing_extensions import TypedDict, Literal 27 | except ImportError: 28 | print(f'Please install the "typing_extensions" module via the shell ' 29 | f'command "# pip install typing_extensions" before running ' 30 | f'{__file__} with Python-versions <= 3.8!') 31 | try: 32 | from typing_extensions import NotRequired, ReadOnly, TypeAlias 33 | except ImportError: 34 | NotRequired = Optional 35 | ReadOnly = Union 36 | TypeAlias = Any 37 | GenericMeta = type 38 | class _GenericTypedDictMeta(GenericMeta): 39 | def __new__(cls, name, bases, ns, total=True): 40 | return type.__new__(_GenericTypedDictMeta, name, (dict,), ns) 41 | __call__ = dict 42 | GenericTypedDict = _GenericTypedDictMeta('TypedDict', (dict,), {}) 43 | GenericTypedDict.__module__ = __name__ 44 | 45 | 46 | source_hash__ = "4b4bdd174f3d8b259dd9dd13a39989e2" 47 | 48 | 49 | ##### BEGIN OF ts2python generated code 50 | 51 | 52 | class TextDocument: 53 | additionalCommonProperties: Optional[Dict[str, Any]] 54 | 55 | def save(self) -> Thenable[bool]: 56 | pass 57 | 58 | T = TypeVar('T') 59 | 60 | PromiseLike = Iterable # Admittedly, a very poor hack 61 | 62 | class Thenable(PromiseLike[T], Generic[T]): 63 | pass 64 | 65 | DocumentSelector = Union[DocumentFilter, str, List[Union[DocumentFilter, str]]] 66 | 67 | 68 | ##### END OF ts2python generated code -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 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 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # PyCharm 132 | .idea/ 133 | .idea/misc.xml 134 | .idea/* 135 | .idea/** 136 | 137 | # DHParser-stuff 138 | REPORT 139 | **/REPORT/* 140 | LOGS 141 | **/LOGS/* 142 | /DHParser/ 143 | 144 | 145 | # Other 146 | # scratch/ 147 | 148 | /.run/ 149 | /demo/ts2python_output/ 150 | -------------------------------------------------------------------------------- /tests/test_singledispatch_shim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """test_singledispatch_shim.py -- test code for ts2python's singledispatch""" 4 | 5 | from __future__ import annotations 6 | 7 | # from functools import singledispatch, singledispatchmethod 8 | import os 9 | import sys 10 | from typing import List, Union 11 | 12 | scriptpath = os.path.abspath(os.path.dirname(__file__) or '.') 13 | ts2pythonpath = os.path.normpath(os.path.join(scriptpath, '..')) 14 | if ts2pythonpath not in sys.path: sys.path.append(ts2pythonpath) 15 | 16 | from ts2python.singledispatch_shim import singledispatch, singledispatchmethod 17 | 18 | 19 | @singledispatch 20 | def func(arg): 21 | raise TypeError(f"Possible types for arg are int or str, not {type(arg)}") 22 | 23 | 24 | @func.register 25 | def _(intarg: int): 26 | assert isinstance(intarg, int) 27 | return int 28 | 29 | 30 | @func.register 31 | def _(strarg: str): 32 | assert isinstance(strarg, str) 33 | return str 34 | 35 | 36 | class TestSingleDispatchShim: 37 | def test_singledispatch(self): 38 | assert func(1) is int 39 | assert func("1") is str 40 | try: 41 | func([1, 2, 3]) 42 | assert False, "TypeError expected" 43 | except TypeError: 44 | pass 45 | 46 | 47 | class A: 48 | @singledispatchmethod 49 | def func(self, param): 50 | pass 51 | 52 | @func.register 53 | def _(self, param: C, a: int): 54 | return a 55 | @func.register 56 | def _(self, b: complex, c: float): 57 | return b, c 58 | 59 | 60 | class B: 61 | @singledispatchmethod 62 | def func(self, param): 63 | pass 64 | 65 | @func.register 66 | def _(self, param: "C", a: int): 67 | return a 68 | @func.register 69 | def _(self, b: complex, c: float): 70 | return b, c 71 | 72 | 73 | class C: 74 | pass 75 | 76 | 77 | # @A.func.register 78 | # def _(self, param: C, a: int): 79 | # return a 80 | # @A.func.register 81 | # def _(self, b: complex, c: float): 82 | # return b, c 83 | 84 | 85 | class TestForwardReference: 86 | def test_forward_reference(self): 87 | a = A() 88 | assert a.func(C(), 3) == 3 89 | assert a.func((3 + 2j), 5.0) == ((3 + 2j), 5.0) 90 | 91 | def test_forward_reference_string_notation(self): 92 | b = B() 93 | assert b.func(C(), 3) == 3 94 | assert b.func((3 + 2j), 5.0) == ((3 + 2j), 5.0) 95 | 96 | 97 | class TestGenericAlias: 98 | def test_generic_alias(self): 99 | @singledispatch 100 | def func(param): 101 | pass 102 | @func.register(list) 103 | def _(param:List['int']): 104 | pass 105 | 106 | 107 | 108 | if __name__ == "__main__": 109 | from runner import runner 110 | runner("", globals()) 111 | -------------------------------------------------------------------------------- /Changes.txt: -------------------------------------------------------------------------------- 1 | Verison 0.8.2 2 | ------------- 3 | - various bugfixes 4 | 5 | Version 0.8.1 6 | ------------- 7 | - bugfixes, mostly related to the represnetation of comments with 8 | the --comments comandline switch 9 | 10 | Version 0.8.0 11 | ------------- 12 | - ts2pythonExplorer: A GUI interfaces for exploring and using ts2python 13 | - Python 3.14 support (via the "--compatibility 3.14" switch) 14 | - Postponed evalaution (PEP 563) and deferred evaluation 15 | (Python 3.14 and higher) is now supported with the "--peps 563" 16 | and "--peps 649" switches, respectively. 17 | - type-statements are now used instead of TypeAlias for Python 18 | compatibility level >= 3.12 19 | - Typescript's "readonly" is now transpiled to Python's "ReadOnly[...]" 20 | for Python compatibility level >= 3.13 21 | - bugfix: configuration-flag UseTypeParameters (for Python 22 | version 3.12 and above) is not ignored, anymore 23 | - Removed --base (Base-Class) and --decorator switches from command line 24 | options. Base-class now always defaults to TypedDict, and no additional 25 | decorators will be added 26 | - ts2pythonParser skript has a new option --target that allows to select 27 | the abstract-syntax-tree as compilation target rahter than the final 28 | Python file 29 | - typo-corrections and some updates to the documentation 30 | 31 | 32 | Version 0.7.6 33 | ------------- 34 | - Typescript Records are not translated to Dict[]-types 35 | - Omitted semicolons do not cause the parser to fail, anymore 36 | 37 | 38 | Version 0.7.5 39 | ------------- 40 | - bugfixes related to comments 41 | - bugs #21, #19 42 | - several changes to make ts2python digest the current vscode.d.ts 43 | 44 | 45 | Version 0.7.4 46 | ------------- 47 | - comments can be preserved: option "-k" or "--comments". Exception: 48 | Inline comments, e.g. "dog /* my little dog */: int", will not be preserved 49 | 50 | 51 | Version 0.7.3 52 | ------------- 53 | - better packaging 54 | 55 | 56 | Version 0.7.2 57 | ------------- 58 | - bugfixes 59 | - more advanced types supported (by the parser only! which 60 | means they are read but may not be translated to Python 61 | very well) 62 | 63 | 64 | Version 0.7.1 65 | ------------- 66 | - rendering of anonymous interfaces as toplevel-TypedDicts now 67 | possible (use switch -a toplevel) to avoid error messages by 68 | type checkers 69 | - added rudimentary support for more advanced types, e.g. 70 | "type Exact = { [K in keyof T]: T[K] }" 71 | Caveat: Not all of these constructs are supported and, if they are supported, 72 | they will appear strongly simplified on the Python side, often simply by 73 | using "Any". But at least some of the constructs will be parsed, now. 74 | 75 | 76 | Version 0.6.9 77 | ------------- 78 | - read (but ignore) TypeScript imports 79 | - add alternative ways to render anonymous interfaces as TypedDicts: 80 | Apart from "local" (default), now "functional" and "type" can 81 | be selected. "type" is still experimental 82 | 83 | 84 | Version 0.6.8 85 | ------------- 86 | 87 | - Use of NotRequired (PEP 655) instead of "Optional" is now default. 88 | - typeddict_shim is not needed, anymore, for Python 3.11 and above. 89 | - compatibility with DHParser 1.4.2 90 | -------------------------------------------------------------------------------- /docs/BasicUsage.rst: -------------------------------------------------------------------------------- 1 | Basic Usage 2 | =========== 3 | 4 | Generating TypeDict-classes from Typescript-Interfaces 5 | ------------------------------------------------------ 6 | 7 | TypedDict-classes can be generated from Typescript-Interfaces, 8 | by running the ``ts2python``-command from the command line on 9 | the Typescript-Interface definitions:: 10 | 11 | $ ts2python interfaces.ts 12 | 13 | This generates a .py-file with the same name (safe for the extension) 14 | in same directory as the source 15 | file that contains the TypedDict-classes and can simpy be 16 | imported in Python-Code:: 17 | 18 | from interfaces import * 19 | 20 | Typescript-interfaces are transformed to Python-TypedDicts 21 | in a straight-forward way. The following Typescript-code:: 22 | 23 | interface Message { 24 | jsonrpc: string; 25 | } 26 | interface RequestMessage extends Message { 27 | id: integer | string; 28 | method: string; 29 | params?: array | object; 30 | } 31 | interface ResponseMessage extends Message { 32 | id: integer | string | null; 33 | result?: string | number | boolean | object | null; 34 | error?: ResponseError; 35 | } 36 | 37 | will become:: 38 | 39 | class Message(TypedDict, total=True): 40 | jsonrpc: str 41 | 42 | class RequestMessage(Message, TypedDict): 43 | id: Union[int, str] 44 | method: str 45 | params: NotRequired[Union[List, Dict]] 46 | 47 | class ResponseMessage(Message, TypedDict): 48 | id: Union[int, str, None] 49 | result: NotRequired[Union[str, float, bool, Dict, None]] 50 | error: NotRequired['ResponseError'] 51 | 52 | 53 | Type-checking Input and Return-Values 54 | ------------------------------------- 55 | 56 | In order to allow static type-checking with `mypy`_ or other 57 | Python type-checkers, `type-annotations`_ should be used in the source 58 | code, e.g.:: 59 | 60 | def process_request(request: RequestMessage) -> ResponseMessage: 61 | ... 62 | return ResponseMessage(id = request.id) 63 | 64 | There are some cases where static type-checking on the Python-side might 65 | not suffice to catch all type errors. For example, if the input data 66 | stems from a JSON-RPC call and is de-serialized via:: 67 | 68 | import json 69 | request_msg: RequestMessage = json.loads(input_data) 70 | 71 | Type-conformance must then be checked at runtime with: 72 | 73 | from ts2python.json_validation import validate_type 74 | validate_type(request_msg, RequestMessage) 75 | 76 | ``validate_type`` will raise a TypeError, if the type is incorrect. 77 | Alternatively, both the call parameters and the return value of a Python 78 | function can be validated at runtime against their annotated types by 79 | adding the ``type_check``-decorator to the function, e.g.:: 80 | 81 | from ts2python.json_validation import type_check 82 | @type_check 83 | def process_request(request: RequestMessage) -> ResponseMessage: 84 | ... 85 | return ResponseMessage(id = request.id) 86 | 87 | Mind that runtime-type validation consumes time. Depending on the 88 | application case, you might consider using it only during development 89 | or for debugging. 90 | 91 | .. _mypy: http://mypy-lang.org/ 92 | .. _type-annotations: https://www.python.org/dev/peps/pep-0484/ 93 | -------------------------------------------------------------------------------- /demo/extract_ts_from_lsp_specs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """extract_ts_from_lsp.py - extracts the typescript parts from the 4 | specification of the language server protocol: 5 | https://github.com/microsoft/language-server-protocol/tree/gh-pages/_specifications 6 | """ 7 | 8 | import re 9 | 10 | 11 | LSP_SPEC_SOURCE = \ 12 | "https://raw.githubusercontent.com/microsoft/language-server-protocol/" \ 13 | "gh-pages/_specifications/lsp/3.18/specification.md" 14 | 15 | 16 | def no_declaration(l): 17 | if l[0:1] in ("{", "["): 18 | return True 19 | if re.match(r'\w+(?:\.\w+)*\(', l): 20 | return True 21 | return False 22 | 23 | 24 | def extract(specs, dest): 25 | lines = specs.split('\n') 26 | ts = [] 27 | copy_flag = False 28 | for l in lines: 29 | if l.strip() == '```typescript': 30 | copy_flag = True 31 | elif l.strip() == '```': 32 | copy_flag = False 33 | ts.append('') 34 | else: 35 | if copy_flag: 36 | if no_declaration(l): 37 | copy_flag = False 38 | elif l[0:2] != "//": 39 | ts.append(l) 40 | with open(dest, 'w', encoding='utf-8') as f: 41 | f.write('\n'.join(ts)) 42 | 43 | 44 | def download_specfile(url: str) -> str: 45 | import urllib.request 46 | max_indirections = 2 47 | while max_indirections > 0: 48 | if url.startswith('http:') or url.startswith('https:'): 49 | print('fetching: ' + url) 50 | with urllib.request.urlopen(url) as f: 51 | specs = f.read() 52 | else: 53 | with open(url, 'rb') as f: 54 | specs = f.read() 55 | if len(specs) < 255 and specs.find(b'\n') < 0: 56 | url = url[: url.rfind('/') + 1] + specs.decode('utf-8').strip() 57 | max_indirections -= 1 58 | else: 59 | max_indirections = 0 60 | return specs.decode('utf-8') 61 | 62 | 63 | RX_INCLUDE = re.compile(r'{%\s*include_relative\s*(?P[A-Za-z0-9/.]+?\.md)\s*%}|{%\s*include\s*(?P[A-Za-z0-9/.]+?\.md)\s*%}') 64 | 65 | 66 | def download_specs(url: str) -> str: 67 | specfile = download_specfile(url) 68 | relurl_path = url[:url.rfind('/') + 1] 69 | absurl_path = url[:url.find('_specifications/')] + '_includes/' 70 | parts = [] 71 | e = 0 72 | for m in RX_INCLUDE.finditer(specfile): 73 | s = m.start() 74 | parts.append(specfile[e:s]) 75 | if m.group('relative') is not None: 76 | incpath = m.group('relative') 77 | incl_url = relurl_path + incpath 78 | else: 79 | assert m.group('absolute') is not None 80 | incpath = m.group('absolute') 81 | incl_url = absurl_path + incpath 82 | parts.append(f'\n```typescript\n\n/* source file: "{incpath}" */\n```\n') 83 | include = download_specs(incl_url) 84 | parts.append(include) 85 | e = m.end() 86 | parts.append(specfile[e:]) 87 | specs = ''.join(parts) 88 | return specs 89 | 90 | 91 | if __name__ == "__main__": 92 | import sys 93 | if len(sys.argv) > 1: 94 | url = sys.argv[1] 95 | if url.startswith('www.'): url = 'https://' + url 96 | else: 97 | url = LSP_SPEC_SOURCE 98 | name = url[url.rfind('/') + 1:] 99 | i = name.rfind('.') 100 | if i < 0: i = len(name) 101 | destname = name[:i] + '.ts' 102 | specs = download_specs(url) 103 | extract(specs, destname) 104 | 105 | -------------------------------------------------------------------------------- /tests_grammar/07_test_Types.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.RenderAnonymous = "local" 3 | 4 | [match:type_alias] 5 | M1: """export type ProviderResult = T | undefined | null | Thenable;""" 6 | M2: """export type SymbolTag = 1;""" 7 | M3: """export type Maybe = T | null;""" 8 | M4: """export type InputMaybe = Maybe;""" 9 | M5: """export type Exact = { [K in keyof T]: T[K] };""" 10 | M6: """export type MakeOptional = Omit & { [SubKey in K]?: Maybe };""" 11 | M7: """export type MakeMaybe = Omit & { [SubKey in K]: Maybe };""" 12 | M8: """export type Maybe = T | null;""" 13 | M9: """type f = (..._: any[]) => Response;""" 14 | 15 | [py:type_alias] 16 | M9: """f = 'Callable[..., Response]'""" 17 | 18 | [match:index_signature] 19 | M1: """[uri: DocumentUri]""" 20 | 21 | [ast:index_signature] 22 | 23 | [fail:index_signature] 24 | 25 | 26 | [match:types] 27 | M1: """(TextDocumentEdit | CreateFile | RenameFile | DeleteFile)[]""" 28 | M2: """( 29 | TextDocumentEdit[] | 30 | (TextDocumentEdit | CreateFile | RenameFile | DeleteFile)[] 31 | )""" 32 | M3: """string | { language: string; value: string }""" 33 | M4: """readonly [number, number] | undefined""" 34 | 35 | [ast:types] 36 | 37 | [fail:types] 38 | 39 | 40 | [match:type] 41 | M1: """[start: number, end: number]""" 42 | 43 | [ast:type] 44 | 45 | [fail:type] 46 | 47 | 48 | [match:generic_type] 49 | M1: """Thenable""" 50 | M2: """T""" 51 | M3: """T, L>""" 52 | M4: """Array""" 53 | M5: """Thenable""" 54 | M6: """Progress<{ message?: string; increment?: number }>""" 55 | 56 | 57 | [match:type_parameters] 58 | M1: """""" 59 | M2: """""" 60 | M3: """""" 61 | 62 | [ast:type_parameters] 63 | 64 | [fail:type_parameters] 65 | 66 | 67 | [match:type_name] 68 | 69 | [ast:type_name] 70 | 71 | [fail:type_name] 72 | 73 | 74 | [match:array_of] 75 | M1: """{ dispose: () => any }[]""" 76 | 77 | [ast:array_of] 78 | 79 | [fail:array_of] 80 | 81 | 82 | [match:type_tuple] 83 | 84 | [ast:type_tuple] 85 | 86 | [fail:type_tuple] 87 | 88 | 89 | [match:mapped_type] 90 | M1: "{ [K in keyof T]: T[K] }" 91 | M2: "{ [SubKey in K]: Maybe }" 92 | M3: "{ [SubKey in K]?: Maybe }" 93 | 94 | [ast:mapped_type] 95 | 96 | [fail:mapped_type] 97 | 98 | 99 | [match:map_signature] 100 | M1: """[gerne: string]: string[]""" 101 | 102 | 103 | [ast:map_signature] 104 | 105 | [fail:map_signature] 106 | 107 | 108 | [match:func_type] 109 | M1: '''(editBuilder: TextEditorEdit) => void''' 110 | M2: '''(...args: any[]) => any''' 111 | M3: '''(textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void''' 112 | 113 | [match:intersection] 114 | M1: '''AuthenticationGetSessionOptions & { forceNewSession: true | { detail: string } }''' 115 | M2: '''WebviewPanelOptions & WebviewOptions''' 116 | 117 | [match:function] 118 | M1: """export function t(message: string, ...args: Array): string""" 119 | M2: """constructor( 120 | uri: Uri, 121 | statementCoverage: TestCoverageCount, 122 | branchCoverage?: TestCoverageCount, 123 | declarationCoverage?: TestCoverageCount, 124 | )""" 125 | 126 | [match:document] 127 | M1: """type integer = number 128 | 129 | export interface Test {}""" 130 | 131 | [match:interface] 132 | M1: """export interface Test { 133 | t: Record 134 | }""" 135 | 136 | M2: """interface ItemList { 137 | [gerne: string]: string[] 138 | }""" -------------------------------------------------------------------------------- /tests_grammar/19_test_compatibility_3_13.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.RenderAnonymous = "local" 3 | ts2python.UsePostponedEvaluation = False 4 | ts2python.UseTypeUnion = True 5 | ts2python.UseExplicitTypeAlias = False 6 | ts2python.UseTypeParameters = True 7 | ts2python.UseLiteralType = True 8 | ts2python.UseVariadicGenerics = True 9 | ts2python.UseNotRequired = True 10 | ts2python.AllowReadOnly = True 11 | ts2python.AssumeDeferredEvaluation = False 12 | 13 | 14 | [match:type_alias] 15 | M1: """export type ProviderResult = T | U | V | undefined | null | Thenable;""" 16 | 17 | [py:type_alias] 18 | M1: """type ProviderResult[T, U, V] = 'ReadOnly[T] | ReadOnly[U] | V | None | Thenable[ReadOnly[T] | ReadOnly[U] | V | None]'""" 19 | 20 | 21 | [match:declaration] 22 | M1: "readonly hoverMessage?: MarkdownString | MarkedString | Array" 23 | M2: "hoverMessage?: readonly MarkdownString | readonly MarkedString | Array" 24 | 25 | [py:declaration] 26 | M1: "hoverMessage: NotRequired[ReadOnly['MarkdownString | MarkedString | List[MarkdownString | MarkedString]']]" 27 | M2: "hoverMessage: NotRequired[ReadOnly['MarkdownString'] | ReadOnly['MarkedString'] | List[ReadOnly['MarkdownString'] | ReadOnly['MarkedString']]]" 28 | 29 | 30 | [match:function] 31 | M1: "function diameter(d: T): number" 32 | M2: "function diameter(d: readonly T): number" 33 | 34 | [py:function] 35 | M1: """ 36 | def diameter[T](d: ReadOnly[T]) -> float: 37 | pass""" 38 | M2: """ 39 | def diameter[T](d: ReadOnly[T]) -> float: 40 | pass""" 41 | 42 | 43 | [match:interface] 44 | M1: """interface Color { 45 | readonly red: decimal; 46 | readonly green: decimal; 47 | readonly blue: decimal; 48 | readonly alpha: decimal; 49 | }""" 50 | M2: """interface Value { 51 | readonly val: integer | decimal | string; 52 | }""" 53 | M3: """interface ProgressParams { 54 | token: ProgressToken; 55 | value: T; 56 | }""" 57 | M4: """interface ProgressParams { 58 | token: ProgressToken; 59 | value: readonly T; 60 | }""" 61 | M5: """interface ProgressParams { 62 | token: ProgressToken; 63 | value: readonly T; 64 | }""" 65 | 66 | [py:interface] 67 | M1: """ 68 | class Color(TypedDict): 69 | red: ReadOnly[float] 70 | green: ReadOnly[float] 71 | blue: ReadOnly[float] 72 | alpha: ReadOnly[float]""" 73 | M2: """ 74 | class Value(TypedDict): 75 | val: ReadOnly[int | float | str]""" 76 | M3: """ 77 | class ProgressParams[T](TypedDict): 78 | token: 'ProgressToken' 79 | value: ReadOnly[T]""" 80 | M4: """ 81 | class ProgressParams[T](TypedDict): 82 | token: 'ProgressToken' 83 | value: ReadOnly[T]""" 84 | M5: """ 85 | class ProgressParams[T](TypedDict): 86 | token: 'ProgressToken' 87 | value: ReadOnly[T]""" 88 | 89 | 90 | [match:document] 91 | M1: """export interface SemanticTokensDelta { 92 | readonly resultId?: string; 93 | edits: SemanticTokensEdit[]; 94 | } 95 | 96 | export interface SemanticTokensEdit { 97 | start: uinteger; 98 | deleteCount: uinteger; 99 | data?: uinteger[]; 100 | }""" 101 | 102 | [py:document] 103 | M1: """ 104 | class SemanticTokensDelta(TypedDict): 105 | resultId: NotRequired[ReadOnly[str]] 106 | edits: List['SemanticTokensEdit'] 107 | 108 | 109 | class SemanticTokensEdit(TypedDict): 110 | start: int 111 | deleteCount: int 112 | data: NotRequired[List[int]]""" 113 | 114 | 115 | [match:root] 116 | M1: "" 117 | 118 | 119 | -------------------------------------------------------------------------------- /tests_grammar/17_test_compatibility_3_11.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.RenderAnonymous = "local" 3 | ts2python.UsePostponedEvaluation = False 4 | ts2python.UseTypeUnion = True 5 | ts2python.UseExplicitTypeAlias = True 6 | ts2python.UseTypeParameters = False 7 | ts2python.UseLiteralType = True 8 | ts2python.UseVariadicGenerics = True 9 | ts2python.UseNotRequired = True 10 | ts2python.AllowReadOnly = False 11 | ts2python.AssumeDeferredEvaluation = False 12 | 13 | 14 | [match:type_alias] 15 | M1: """export type ProviderResult = T | undefined | null | Thenable;""" 16 | 17 | [py:type_alias] 18 | M1: """T = TypeVar('T') 19 | ProviderResult: TypeAlias = 'T | None | Thenable[T | None]' 20 | """ 21 | 22 | [match:function] 23 | M1: "function diameter(d: T): number" 24 | M2: "function diameter(d: readonly T): number" 25 | 26 | [py:function] 27 | M1: """ 28 | T = TypeVar('T') 29 | def diameter(d: T) -> float: 30 | pass""" 31 | M2: """ 32 | T = TypeVar('T') 33 | def diameter(d: T) -> float: 34 | pass""" 35 | 36 | [match:interface] 37 | M1: """interface ProgressParams { 38 | token: ProgressToken; 39 | value: T; 40 | }""" 41 | M2: """export interface Event { 42 | (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable; 43 | }""" 44 | M3: """export class EventEmitter { 45 | event: Event; 46 | fire(data: T): void; 47 | dispose(): void; 48 | }""" 49 | 50 | [py:interface] 51 | M1: """T = TypeVar('T') 52 | 53 | class ProgressParams(TypedDict): 54 | token: 'ProgressToken' 55 | value: T 56 | """ 57 | M2: """T = TypeVar('T') 58 | 59 | class Event(Generic[T]): 60 | 61 | def __call__(self, listener: Callable[[T], Any], thisArgs: Optional[Any] = None, disposables: Optional[List['Disposable']] = None) -> 'Disposable': 62 | pass 63 | """ 64 | M3: """T = TypeVar('T') 65 | 66 | class EventEmitter(Generic[T]): 67 | event: 'Event[T]' 68 | 69 | def fire(self, data: T) -> None: 70 | pass 71 | 72 | def dispose(self) -> None: 73 | pass 74 | """ 75 | 76 | [match:document] 77 | M1: """export namespace SymbolKind { 78 | export const File = 1; 79 | export const Module = 2; 80 | export const Namespace = 3; 81 | export const Package = 4; 82 | export const Class = 5; 83 | export const Method = 6; 84 | export const Property = 7; 85 | export const Field = 8; 86 | export const Constructor = 9; 87 | export const Enum = 10; 88 | export const Interface = 11; 89 | export const Function = 12; 90 | export const Variable = 13; 91 | export const Constant = 14; 92 | export const String = 15; 93 | export const Number = 16; 94 | export const Boolean = 17; 95 | export const Array = 18; 96 | export const Object = 19; 97 | export const Key = 20; 98 | export const Null = 21; 99 | export const EnumMember = 22; 100 | export const Struct = 23; 101 | export const Event = 24; 102 | export const Operator = 25; 103 | export const TypeParameter = 26; 104 | } 105 | 106 | export type SymbolKind = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 107 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26;""" 108 | 109 | [py:document] 110 | M1: """class SymbolKind(IntEnum): 111 | File = 1 112 | Module = 2 113 | Namespace = 3 114 | Package = 4 115 | Class = 5 116 | Method = 6 117 | Property = 7 118 | Field = 8 119 | Constructor = 9 120 | Enum = 10 121 | Interface = 11 122 | Function = 12 123 | Variable = 13 124 | Constant = 14 125 | String = 15 126 | Number = 16 127 | Boolean = 17 128 | Array = 18 129 | Object = 19 130 | Key = 20 131 | Null = 21 132 | EnumMember = 22 133 | Struct = 23 134 | Event = 24 135 | Operator = 25 136 | TypeParameter = 26 137 | 138 | # commented out, because there is already an enumeration with the same name 139 | # SymbolKind: TypeAlias = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]""" 140 | 141 | [match:root] 142 | M1: "" 143 | -------------------------------------------------------------------------------- /tests_grammar/18_test_compatibility_3_12.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.RenderAnonymous = "local" 3 | ts2python.UsePostponedEvaluation = False 4 | ts2python.UseTypeUnion = True 5 | ts2python.UseExplicitTypeAlias = False 6 | ts2python.UseTypeParameters = True 7 | ts2python.UseLiteralType = True 8 | ts2python.UseVariadicGenerics = True 9 | ts2python.UseNotRequired = True 10 | ts2python.AllowReadOnly = False 11 | ts2python.AssumeDeferredEvaluation = False 12 | 13 | 14 | [match:type_alias] 15 | M1: """export type LSPAny = LSPObject | LSPArray | string | integer | uinteger | 16 | decimal | boolean | null;""" 17 | 18 | [py:type_alias] 19 | M1: """type LSPAny = 'LSPObject | LSPArray | str | int | float | bool | None'""" 20 | 21 | 22 | [match:function] 23 | M1: "function diameter(d: T): number" 24 | M2: "function diameter(d: readonly T): number" 25 | 26 | [py:function] 27 | M1: """ 28 | def diameter[T](d: T) -> float: 29 | pass""" 30 | M2: """ 31 | def diameter[T](d: T) -> float: 32 | pass""" 33 | 34 | 35 | [match:interface] 36 | M1: """interface OptionalValue { 37 | option?: integer | string; 38 | }""" 39 | M2: """interface ProgressParams { 40 | token: ProgressToken; 41 | value: T; 42 | }""" 43 | M3: """interface PromiseLike { 44 | then( 45 | onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, 46 | onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null 47 | ): PromiseLike; 48 | }""" 49 | M4: '''export interface WorkspaceConfiguration { 50 | inspect(section: string): { 51 | key: string; 52 | defaultValue?: T; 53 | languageIds?: string[]; 54 | } | undefined; 55 | readonly [key: string]: any; 56 | }''' 57 | 58 | [py:interface] 59 | M1: """class OptionalValue(TypedDict): 60 | option: NotRequired[int | str]""" 61 | M2: """class ProgressParams[T](TypedDict): 62 | token: 'ProgressToken' 63 | value: T""" 64 | M3: """class PromiseLike[T]: 65 | 66 | def then[TResult1, TResult2](self, onfulfilled: Optional[Callable[[T], 'TResult1 | PromiseLike[TResult1]'] | None] = None, onrejected: Optional[Callable[[Any], 'TResult2 | PromiseLike[TResult2]'] | None] = None) -> 'PromiseLike[TResult1 | TResult2]': 67 | pass""" 68 | M4: """class WorkspaceConfiguration: 69 | class InspectWorkspaceConfiguration_0[T](TypedDict): 70 | key: str 71 | defaultValue: NotRequired[T] 72 | languageIds: NotRequired[List[str]] 73 | 74 | def inspect[T](self, section: str) -> InspectWorkspaceConfiguration_0 | None: 75 | pass""" 76 | 77 | 78 | [match:document] 79 | M1: """namespace Window { 80 | export function withProgress(options: ProgressOptions, task: (progress: Progress<{ 81 | message?: string; 82 | increment?: number; 83 | }>, token: CancellationToken) => Thenable): Thenable; 84 | }""" 85 | M2: """export interface TextDocument { 86 | readonly additionalCommonProperties?: Record; 87 | save(): Thenable; 88 | } 89 | 90 | 91 | interface Thenable extends PromiseLike { } 92 | 93 | export type DocumentSelector = DocumentFilter | string | ReadonlyArray;""" 94 | 95 | 96 | [py:document] 97 | M1: """ 98 | class Window: 99 | class Progress_0[R](TypedDict): 100 | message: NotRequired[str] 101 | increment: NotRequired[float] 102 | 103 | def withProgress[R](options: 'ProgressOptions', task: Callable[['Progress[Progress_0]', 'CancellationToken'], 'Thenable[R]']) -> 'Thenable[R]': 104 | pass""" 105 | 106 | M2: """class TextDocument: 107 | additionalCommonProperties: Optional[Dict[str, Any]] 108 | 109 | def save(self) -> 'Thenable[bool]': 110 | pass 111 | 112 | 113 | class PromiseLike[T]: 114 | def then(self, onfullfilled: Optional[Callable], onrejected: Optional[Callable]) -> Self: 115 | pass 116 | 117 | 118 | class Thenable[T](PromiseLike[T]): 119 | pass 120 | 121 | type DocumentSelector = 'DocumentFilter | str | List[DocumentFilter | str]' 122 | """ 123 | 124 | [match:root] 125 | M1: "" 126 | -------------------------------------------------------------------------------- /tests_grammar/Playground.ini: -------------------------------------------------------------------------------- 1 | 2 | [config] 3 | ts2python.RenderAnonymous = "local" 4 | ts2python.UsePostponedEvaluation = True 5 | ts2python.UseTypeUnion = True 6 | ts2python.UseExplicitTypeAlias = True 7 | ts2python.UseTypeParameters = False 8 | ts2python.UseLiteralType = True 9 | ts2python.UseVariadicGenerics = True 10 | ts2python.UseNotRequired = True 11 | ts2python.AllowReadOnly = True 12 | ts2python.AssumeDeferredEvaluation = False 13 | ts2python.KeepMultilineComments = True 14 | 15 | [match:document] 16 | M1: '''interface ResponseError { 17 | /** 18 | * A number indicating the error type that occurred. 19 | */ 20 | code: integer; 21 | 22 | /** 23 | * A string providing a short description of the error. 24 | */ 25 | message: string; 26 | 27 | /** 28 | * A primitive or structured value that contains additional 29 | * information about the error. Can be omitted. 30 | */ 31 | data?: LSPAny; 32 | } 33 | 34 | export namespace ErrorCodes { 35 | // Defined by JSON-RPC 36 | export const ParseError: integer = -32700; 37 | export const InvalidRequest: integer = -32600; 38 | export const MethodNotFound: integer = -32601; 39 | export const InvalidParams: integer = -32602; 40 | export const InternalError: integer = -32603; 41 | 42 | /** 43 | * This is the start range of JSON-RPC reserved error codes. 44 | * It doesn't denote a real error code. No LSP error codes should 45 | * be defined between the start and end range. For backwards 46 | * compatibility the `ServerNotInitialized` and the `UnknownErrorCode` 47 | * are left in the range. 48 | * 49 | * @since 3.16.0 50 | */ 51 | export const jsonrpcReservedErrorRangeStart: integer = -32099; 52 | /** @deprecated use jsonrpcReservedErrorRangeStart */ 53 | export const serverErrorStart: integer = jsonrpcReservedErrorRangeStart; 54 | 55 | /** 56 | * Error code indicating that a server received a notification or 57 | * request before the server received the `initialize` request. 58 | */ 59 | export const ServerNotInitialized: integer = -32002; 60 | export const UnknownErrorCode: integer = -32001; 61 | 62 | /** 63 | * This is the end range of JSON-RPC reserved error codes. 64 | * It doesn't denote a real error code. 65 | * 66 | * @since 3.16.0 67 | */ 68 | export const jsonrpcReservedErrorRangeEnd = -32000; 69 | /** @deprecated use jsonrpcReservedErrorRangeEnd */ 70 | export const serverErrorEnd: integer = jsonrpcReservedErrorRangeEnd; 71 | 72 | /** 73 | * This is the start range of LSP reserved error codes. 74 | * It doesn't denote a real error code. 75 | * 76 | * @since 3.16.0 77 | */ 78 | export const lspReservedErrorRangeStart: integer = -32899; 79 | 80 | /** 81 | * A request failed but it was syntactically correct, e.g the 82 | * method name was known and the parameters were valid. The error 83 | * message should contain human readable information about why 84 | * the request failed. 85 | * 86 | * @since 3.17.0 87 | */ 88 | export const RequestFailed: integer = -32803; 89 | 90 | /** 91 | * The server cancelled the request. This error code should 92 | * only be used for requests that explicitly support being 93 | * server cancellable. 94 | * 95 | * @since 3.17.0 96 | */ 97 | export const ServerCancelled: integer = -32802; 98 | 99 | /** 100 | * The server detected that the content of a document got 101 | * modified outside normal conditions. A server should 102 | * NOT send this error code if it detects a content change 103 | * in its unprocessed messages. The result even computed 104 | * on an older state might still be useful for the client. 105 | * 106 | * If a client decides that a result is not of any use anymore 107 | * the client should cancel the request. 108 | */ 109 | export const ContentModified: integer = -32801; 110 | 111 | /** 112 | * The client has canceled a request and a server has detected 113 | * the cancel. 114 | */ 115 | export const RequestCancelled: integer = -32800; 116 | 117 | /** 118 | * This is the end range of LSP reserved error codes. 119 | * It doesn't denote a real error code. 120 | * 121 | * @since 3.16.0 122 | */ 123 | export const lspReservedErrorRangeEnd: integer = -32800; 124 | }''' 125 | 126 | -------------------------------------------------------------------------------- /docs/Validation.rst: -------------------------------------------------------------------------------- 1 | .. _runtime_validation: 2 | 3 | Runtime Validation 4 | ================== 5 | 6 | With ``TypedDict``, any static type checker that already supports 7 | TypedDicts can be leveraged to check the classes generated 8 | by ts2python. 9 | 10 | However, there are use-cases where dynamic type checking of 11 | TypedDicts might be relevant. For example, when processing 12 | json-data stemming from an external source which might 13 | happen to provide invalid data. 14 | 15 | Also, up to Python 3.10 ``TypedDict`` does not allow marking 16 | individual items as required or not required. (See 17 | `PEP 655`_ for the details.) Static type checkers 18 | that do not evaluate the ``Required`` and ``NotRequired`` annotation 19 | will produce false results for TypedDicts that contain not-required 20 | fields. 21 | 22 | Module :py:mod:`ts2python.json_validation` provides functions 23 | and function annotations to validate (arbitrarily nested) typed dicts. 24 | In order to use runtime type-checking, :py:mod:`ts2python.json_validation` 25 | provides shims for ``TypedDict``, ``GenericTypedDict`` and ``NotRequired`` that 26 | should be imported instead of Python's ``typing.TypedDict`` 27 | and ``typing.GenericTypedDict``. Runtime json-Validation 28 | can fail with obscure error messages, if the TypedDict-classes 29 | against which values are 30 | checked at runtime do not derive from 31 | :py:class:`ts2python.json_validation.TypedDict`! 32 | 33 | Validation with decorators 34 | -------------------------- 35 | 36 | The easiest way to use runtime type checking is by adding the 37 | :py:func:`json_validation.type_check`-annotation to a function 38 | receiving or returning a TypedDict:: 39 | 40 | >>> from ts2python.json_validation import TypedDict, type_check 41 | >>> class Position(TypedDict, total=True): 42 | ... line: int 43 | ... character: int 44 | >>> class Range(TypedDict, total=True): 45 | ... start: Position 46 | ... end: Position 47 | >>> @type_check 48 | ... def line_too_long(rng: Range) -> bool: 49 | ... return (rng['start']['character'] > 255 50 | ... or rng['end']['character'] > 255) 51 | >>> line_too_long({'start': {'line': 1, 'character': 1}, 52 | ... 'end': {'line': 8, 'character': 17}}) 53 | False 54 | >>> try: 55 | ... line_too_long({'start': {'line': 1, 'character': 1}, 56 | ... 'end': 256}) 57 | ... except TypeError as e: 58 | ... print(e) 59 | Parameter "rng" of function "line_too_long" failed the type-check, because: 60 | Type error(s) in dictionary of type : 61 | Field end: '256' is not of , but of type 62 | 63 | By default the :py:func:`json_validation.type_check`-annotation validates 64 | both the arguments of a function and its return value. (This behaviour 65 | can be configured with the ``check_return_type``-parameter of the annotation.) 66 | Type validation will not take place on arguments or return values for which 67 | no type annotation is given. 68 | 69 | validate_type-function 70 | ---------------------- 71 | 72 | Alternatively, types can be validated by the calling 73 | :py:func:`json_validation.validate_type`. ``validate_type()`` 74 | does not return anything but either raises a TypeError if 75 | the given value does not have the expected type:: 76 | 77 | >>> from ts2python.json_validation import validate_type 78 | >>> validate_type({'line': 42, 'character': 11}, Position) 79 | >>> try: 80 | ... validate_type({'line': 42, 'character': "bad mistake"}, Position) 81 | ... except TypeError as e: 82 | ... print(e) 83 | Type error(s) in dictionary of type : 84 | Field character: 'bad mistak...' is not a , but a 85 | 86 | The ``validate_type``- and the ``type_check``-annotation will likewise 87 | complain about missing required fields and superfluous fields:: 88 | 89 | >>> from ts2python.json_validation import NotRequired 90 | >>> class Car(TypedDict, total=True): 91 | ... brand: str 92 | ... speed: int 93 | ... color: NotRequired[str] 94 | >>> @type_check 95 | ... def print_car(car: Car): 96 | ... print('brand: ', car['brand']) 97 | ... print('speed: ', car['speed']) 98 | ... if 'color' in car: 99 | ... print('color: ', car['color']) 100 | >>> print_car({'brand': 'Mercedes', 'speed': 200}) 101 | brand: Mercedes 102 | speed: 200 103 | >>> print_car({'brand': 'BMW', 'speed': 180, 'color': 'blue'}) 104 | brand: BMW 105 | speed: 180 106 | color: blue 107 | >>> try: 108 | ... print_car({'speed': 200}) 109 | ... except TypeError as e: 110 | ... print(e) 111 | Parameter "car" of function "print_car" failed the type-check, because: 112 | Type error(s) in dictionary of type : 113 | Missing required keys: {'brand'} 114 | >>> try: 115 | ... print_car({'brand': 'Mercedes', 'speed': 200, 'PS': 120}) 116 | ... except TypeError as e: 117 | ... print(e) 118 | Parameter "car" of function "print_car" failed the type-check, because: 119 | Type error(s) in dictionary of type : 120 | Unexpected keys: {'PS'} 121 | 122 | Type validation works its way up from the root type down to any nested 123 | object. Type unions, e.g. ``int|str`` are evaluated by trying all 124 | alternatives on the data until one alternative matches. Enums and 125 | uniform sequences (e.g. List[str]) are properly taken care of. 126 | 127 | Reference 128 | --------- 129 | 130 | .. automodule:: json_validation 131 | :members: 132 | 133 | .. _PEP 655: https://www.python.org/dev/peps/pep-0655/ 134 | -------------------------------------------------------------------------------- /tst_ts2python_grammar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """tst_ts2python_grammar.py - runs the unit tests for the ts2python-grammar 4 | """ 5 | 6 | import os 7 | import sys 8 | 9 | LOGGING = 'LOGS' 10 | DEBUG = True 11 | TEST_DIRNAME = 'tests_grammar' 12 | 13 | try: 14 | scriptdir = os.path.dirname(os.path.realpath(__file__)) 15 | except NameError: 16 | scriptdir = '' 17 | if scriptdir and scriptdir not in sys.path: sys.path.append(scriptdir) 18 | 19 | try: 20 | from DHParser import versionnumber 21 | except (ImportError, ModuleNotFoundError): 22 | i = scriptdir.rfind("/DHParser/") 23 | if i >= 0: 24 | dhparserdir = scriptdir[:i + 10] # 10 = len("/DHParser/") 25 | if dhparserdir not in sys.path: sys.path.insert(0, dhparserdir) 26 | 27 | 28 | try: 29 | from DHParser.configuration import access_presets, set_preset_value, \ 30 | finalize_presets, get_config_value, ALLOWED_PRESET_VALUES 31 | from DHParser import dsl 32 | import DHParser.log 33 | from DHParser import testing 34 | except ModuleNotFoundError: 35 | print('Could not import DHParser. Please adjust sys.path in file ' 36 | '"%s" manually' % __file__) 37 | sys.exit(1) 38 | 39 | 40 | def recompile_grammar(grammar_src, force): 41 | grammar_tests_dir = os.path.join(scriptdir, TEST_DIRNAME) 42 | testing.create_test_templates(grammar_src, grammar_tests_dir) 43 | # recompiles Grammar only if it has changed 44 | first_run = not os.path.exists(os.path.splitext(grammar_src)[0] + 'Parser.py') 45 | if not dsl.recompile_grammar(grammar_src, force=force, 46 | notify=lambda: print('recompiling ' + grammar_src)): 47 | msg = f'Errors while recompiling "%s":' % os.path.basename(grammar_src) 48 | print('\n'.join(['', msg, '-'*len(msg)])) 49 | error_path = os.path.join(grammar_src[:-5] + '_ebnf_MESSAGES.txt') 50 | with open(error_path, 'r', encoding='utf-8') as f: 51 | print(f.read()) 52 | sys.exit(1) 53 | if first_run: dsl.create_scripts(grammar_src) 54 | 55 | 56 | def run_grammar_tests(fn_pattern, parser_factory, transformer_factory, 57 | junctions=set(), targets=set(), serializations=set()): 58 | if fn_pattern.find('/') >= 0 or fn_pattern.find('\\') >= 0: 59 | testdir, fn_pattern = os.path.split(fn_pattern) 60 | if not testdir.startswith('/') or not testdir[1:2] == ':': 61 | testdir = os.path.abspath(testdir) 62 | else: 63 | testdir = os.path.join(scriptdir, TEST_DIRNAME) 64 | DHParser.log.start_logging(os.path.join(testdir, LOGGING)) 65 | error_report = testing.grammar_suite( 66 | testdir, parser_factory, transformer_factory, 67 | fn_patterns=[fn_pattern], report='REPORT', verbose=True, 68 | junctions=junctions, show=targets, serializations=serializations) 69 | return error_report 70 | 71 | 72 | if __name__ == '__main__': 73 | from argparse import ArgumentParser 74 | parser = ArgumentParser(description='Runs all grammar-tests in "test_grammar/" ' 75 | 'or a given test - after (re-)creating the parser script if necessary.') 76 | parser.add_argument('files', nargs='*') 77 | parser.add_argument('-s', '--scripts', action='store_const', const='scripts', 78 | help="Creates a server- and an app-script. Existing scripts will not be overwritten!") 79 | parser.add_argument('--singlethread', action='store_const', const='singlethread', 80 | help='Run tests in a single thread (recommended only for debugging)') 81 | parser.add_argument('-p', '--history', action='store_const', const='history', 82 | help="Detailed logs for of the parsing-history for all tests.") 83 | parser.add_argument('-n', '--nohistory', action='store_const', const='nohistory', 84 | help="Deprecated argument") 85 | parser.add_argument('-d', '--debug', action='store_const', const='debug', 86 | help='Deprecated argument.') 87 | args = parser.parse_args() 88 | 89 | if args.debug is not None: 90 | print('Argument -d or --debug is deprecated! Parsing-histories of failed tests will ' 91 | 'be logged per default. Use -p or --history to log all tests.') 92 | if args.nohistory is not None: 93 | print('Argument -n or --nohistory is deprecated! Only parsing-histories of failed ' 94 | 'tests will be logged. Use -p or --history to log all tests.') 95 | 96 | from DHParser.configuration import read_local_config 97 | read_local_config(os.path.join(scriptdir, 'ts2pythonConfig.ini')) 98 | 99 | config_test_parallelization = get_config_value('test_parallelization') 100 | access_presets() 101 | if args.singlethread: 102 | set_preset_value('test_parallelization', False) 103 | elif not config_test_parallelization: 104 | print('Tests will be run in a single-thread, because test-multiprocessing ' 105 | 'has been turned off in configuration file.') 106 | set_preset_value('history_tracking', args.history) 107 | finalize_presets() 108 | 109 | if args.scripts: 110 | dsl.create_scripts(os.path.join(scriptdir, 'ts2python.ebnf')) 111 | 112 | if args.files: 113 | # if called with a single filename that is either an EBNF file or a known 114 | # test file-type then use the given argument 115 | arg = args.files[0] 116 | else: 117 | # otherwise run all tests in the test directory 118 | arg = '*test*.ini' 119 | if arg.endswith('.ebnf'): 120 | recompile_grammar(arg, force=True) 121 | else: 122 | recompile_grammar(os.path.join(scriptdir, 'ts2python.ebnf'), 123 | force=False) 124 | sys.path.append('.') 125 | from ts2pythonParser import parsing, ASTTransformation, \ 126 | junctions, test_targets, serializations 127 | error_report = run_grammar_tests(arg, parsing.factory, ASTTransformation.factory, 128 | junctions, test_targets, serializations) 129 | if error_report: 130 | print('\n') 131 | print(error_report) 132 | sys.exit(1) 133 | print('ready.\n') 134 | -------------------------------------------------------------------------------- /tests/test_ts2python_fwrefs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """test_ts2python_fwrefs.py -- test code for forward referencing and recursive types.""" 4 | 5 | 6 | 7 | 8 | from enum import IntEnum 9 | import os 10 | import sys 11 | from typing import TypeVar, Generic, Union, Dict, List, Optional 12 | 13 | scriptdir = os.path.dirname(os.path.abspath(__file__)) 14 | scriptdir_parent = os.path.abspath(os.path.join(scriptdir, '..')) 15 | 16 | try: 17 | import ts2pythonParser 18 | from ts2pythonParser import compile_src 19 | from ts2python import json_validation 20 | from ts2python.json_validation import validate_type, type_check, \ 21 | validate_uniform_sequence 22 | from ts2python import typeddict_shim 23 | from ts2python.typeddict_shim import TypedDict, NotRequired 24 | except ImportError: 25 | if scriptdir_parent not in sys.path: 26 | sys.path.append(scriptdir_parent) 27 | import ts2pythonParser 28 | from ts2pythonParser import compile_src 29 | from ts2python import json_validation 30 | from ts2python.json_validation import validate_type, type_check, \ 31 | validate_uniform_sequence 32 | from ts2python import typeddict_shim 33 | from ts2python.typeddict_shim import TypedDict, NotRequired 34 | 35 | 36 | ## TEST CLASSES 37 | 38 | integer = float 39 | 40 | uinteger = float 41 | 42 | decimal = float 43 | 44 | LSPAny = Union['LSPObject', 'LSPArray', str, int, float, bool, None] 45 | 46 | LSPObject = Dict[str, LSPAny] 47 | 48 | LSPArray = List[LSPAny] 49 | 50 | 51 | class Message(TypedDict, total=True): 52 | jsonrpc: str 53 | 54 | 55 | class RequestMessage(Message, TypedDict, total=False): 56 | id: Union[int, str] 57 | method: str 58 | params: Union[List, Dict, None] 59 | 60 | 61 | class ResponseMessage(Message, TypedDict, total=False): 62 | id: Union[int, str, None] 63 | result: NotRequired[LSPAny] 64 | error: NotRequired['ResponseError'] 65 | 66 | 67 | class ResponseError(TypedDict, total=True): 68 | code: int 69 | message: str 70 | data: NotRequired[LSPAny] 71 | 72 | class ErrorCodes(IntEnum): 73 | ParseError = -32700 74 | InvalidRequest = -32600 75 | MethodNotFound = -32601 76 | InvalidParams = -32602 77 | InternalError = -32603 78 | jsonrpcReservedErrorRangeStart = -32099 79 | serverErrorStart = jsonrpcReservedErrorRangeStart 80 | ServerNotInitialized = -32002 81 | UnknownErrorCode = -32001 82 | jsonrpcReservedErrorRangeEnd = -32000 83 | serverErrorEnd = jsonrpcReservedErrorRangeEnd 84 | lspReservedErrorRangeStart = -32899 85 | RequestFailed = -32803 86 | ServerCancelled = -32802 87 | ContentModified = -32801 88 | RequestCancelled = -32800 89 | lspReservedErrorRangeEnd = -32800 90 | 91 | class Position(TypedDict, total=True): 92 | line: int 93 | character: int 94 | 95 | 96 | ### END OF TEST-CLASSES 97 | 98 | 99 | class TestValidation: 100 | def setup_class(self): 101 | pass 102 | 103 | def test_type_check(self): 104 | @type_check 105 | def type_checked_func(select_test: int, request: RequestMessage, position: Position) \ 106 | -> ResponseMessage: 107 | validate_type(position, Position) 108 | if select_test == 1: 109 | return {'jsonrpc': 'jsonrpc-string', 110 | 'id': request['id'], 111 | 'error': {'code': -404, 'message': 'bad mistake'}} 112 | elif select_test == 2: 113 | # missing required field 'message' in the contained 114 | # error object should case an error 115 | return {'jsonrpc': 'jsonrpc-string', 116 | 'id': request['id'], 117 | 'error': {'code': -404}} 118 | elif select_test == 3: 119 | return {'jsonrpc': 'Response', 120 | 'id': request['id'], 121 | 'result': "All's well that ends well"} 122 | else: 123 | # Just a different way of creating the dictionary 124 | return ResponseMessage(jsonrpc='Response', id=request['id'], 125 | result="All's well that ends well") 126 | 127 | response = type_checked_func(0, {'jsonrpc': '2.0', 'id': 21, 'method': 'check'}, 128 | Position(line=21, character=15)) 129 | assert response['id'] == 21 130 | response = type_checked_func(1, {'jsonrpc': '2.0', 'id': 21, 'method': 'check'}, 131 | Position(line=21, character=15)) 132 | assert response['id'] == 21 133 | response = type_checked_func(3, RequestMessage(jsonrpc='2.0', id=21, method='check'), 134 | {'line': 21, 'character': 15}) 135 | assert response['id'] == 21 136 | if sys.version_info < (3, 7, 0): 137 | return 138 | try: 139 | _ = type_checked_func(0, {'jsonrpc': '2.0', 'id': 21, 'method': 'check'}) 140 | assert False, "Missing parameter not noticed" 141 | except TypeError: 142 | pass 143 | try: 144 | _ = type_checked_func(0, {'id': 21, 'method': 'check'}, 145 | Position(line=21, character=15)) 146 | assert False, "Type Error in parameter not detected" 147 | except KeyError: 148 | if sys.version_info >= (3, 8): 149 | assert False, "Type Error in parameter not detected" 150 | except TypeError as e: 151 | pass 152 | try: 153 | _ = type_checked_func(2, {'jsonrpc': '2.0', 'id': 21, 'method': 'check'}, 154 | Position(line=21, character=15)) 155 | if sys.version_info >= (3, 8): 156 | assert False, "Type Error in nested return type not detected" 157 | except TypeError: 158 | pass 159 | 160 | 161 | DEFINITION_AFTERT_USAGE_TS = ''' 162 | class Range { 163 | start: Position; 164 | end: Position; 165 | } 166 | 167 | class Position { 168 | line: number; 169 | column: number; 170 | } 171 | ''' 172 | 173 | 174 | class TestClassDefinitionOrder: 175 | def test_class_definition_order(self): 176 | pycode, err = compile_src(DEFINITION_AFTERT_USAGE_TS) 177 | exec(pycode) 178 | 179 | def test_recursive_definition(self): 180 | pass 181 | 182 | 183 | if __name__ == "__main__": 184 | from runner import runner 185 | runner("", globals()) 186 | -------------------------------------------------------------------------------- /scratch/typeddict_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Generic, TypeVar, ClassVar, Any, NoReturn, _SpecialForm, ForwardRef 3 | try: 4 | from typing import Protocol, Final 5 | except ImportError: 6 | Protocol = Generic 7 | Final = type 8 | 9 | 10 | def _type_convert(arg, module=None): 11 | """For converting None to type(None), and strings to ForwardRef.""" 12 | if arg is None: 13 | return type(None) 14 | if isinstance(arg, str): 15 | fwref = ForwardRef(arg) 16 | if hasattr(fwref, '__forward_module__'): 17 | fwref.__forward_module__ = module 18 | return fwref 19 | return arg 20 | 21 | 22 | def _type_check(arg, msg, is_argument=True, module=None): 23 | """Check that the argument is a type, and return it (internal helper). 24 | As a special case, accept None and return type(None) instead. Also wrap strings 25 | into ForwardRef instances. Consider several corner cases, for example plain 26 | special forms like Union are not valid, while Union[int, str] is OK, etc. 27 | The msg argument is a human-readable error message, e.g:: 28 | "Union[arg, ...]: arg should be a type." 29 | We append the repr() of the actual value (truncated to 100 chars). 30 | """ 31 | invalid_generic_forms = (Generic, Protocol) 32 | if is_argument: 33 | invalid_generic_forms = invalid_generic_forms + (ClassVar, Final) 34 | 35 | arg = _type_convert(arg, module=module) 36 | # if (isinstance(arg, _GenericAlias) and 37 | # arg.__origin__ in invalid_generic_forms): 38 | # raise TypeError(f"{arg} is not valid as type argument") 39 | if arg in (Any, NoReturn): 40 | return arg 41 | if (sys.version_info >= (3, 7) and isinstance(arg, _SpecialForm)) \ 42 | or arg in (Generic, Protocol): 43 | raise TypeError(f"Plain {arg} is not valid as type argument") 44 | if isinstance(arg, (type, TypeVar, ForwardRef)): 45 | return arg 46 | if sys.version_info >= (3, 10): 47 | from types import UnionType 48 | if isinstance(arg, UnionType): 49 | return arg 50 | if not callable(arg): 51 | print(sys.version_info, sys.version_info >= (3, 9)) 52 | raise TypeError(f"{msg} Got {arg!r:.100}.") 53 | return arg 54 | 55 | 56 | class _TypedDictMeta(type): 57 | def __new__(cls, name, bases, ns, total=True): 58 | """Create new typed dict class object. 59 | 60 | This method is called when TypedDict is subclassed, 61 | or when TypedDict is instantiated. This way 62 | TypedDict supports all three syntax forms described in its docstring. 63 | Subclasses and instances of TypedDict return actual dictionaries. 64 | """ 65 | for base in bases: 66 | if type(base) is not _TypedDictMeta and base is not Generic: 67 | raise TypeError('cannot inherit from both a TypedDict type ' 68 | 'and a non-TypedDict base class') 69 | tp_dict = type.__new__(_TypedDictMeta, name, (dict,), ns) 70 | 71 | annotations = {} 72 | own_annotations = ns.get('__annotations__', {}) 73 | own_annotation_keys = set(own_annotations.keys()) 74 | msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" 75 | own_annotations = { 76 | n: _type_check(tp, msg, module=tp_dict.__module__) 77 | for n, tp in own_annotations.items() 78 | } 79 | required_keys = set() 80 | optional_keys = set() 81 | 82 | for base in bases: 83 | annotations.update(base.__dict__.get('__annotations__', {})) 84 | required_keys.update(base.__dict__.get('__required_keys__', ())) 85 | optional_keys.update(base.__dict__.get('__optional_keys__', ())) 86 | 87 | annotations.update(own_annotations) 88 | if total: 89 | required_keys.update(own_annotation_keys) 90 | else: 91 | optional_keys.update(own_annotation_keys) 92 | 93 | tp_dict.__annotations__ = annotations 94 | tp_dict.__required_keys__ = frozenset(required_keys) 95 | tp_dict.__optional_keys__ = frozenset(optional_keys) 96 | if not hasattr(tp_dict, '__total__'): 97 | tp_dict.__total__ = total 98 | return tp_dict 99 | 100 | def __getitem__(self, *args, **kwargs): 101 | pass 102 | 103 | __call__ = dict # static method 104 | 105 | def __subclasscheck__(cls, other): 106 | # Typed dicts are only for static structural subtyping. 107 | raise TypeError('TypedDict does not support instance and class checks') 108 | 109 | __instancecheck__ = __subclasscheck__ 110 | 111 | 112 | def TypedDict(typename, fields=None, *, total=True, **kwargs): 113 | """A simple typed namespace. At runtime it is equivalent to a plain dict. 114 | 115 | TypedDict creates a dictionary type that expects all of its 116 | instances to have a certain set of keys, where each key is 117 | associated with a value of a consistent type. This expectation 118 | is not checked at runtime but is only enforced by type checkers. 119 | Usage:: 120 | 121 | class Point2D(TypedDict): 122 | x: int 123 | y: int 124 | label: str 125 | 126 | a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK 127 | b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check 128 | 129 | assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first') 130 | 131 | The type info can be accessed via the Point2D.__annotations__ dict, and 132 | the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets. 133 | TypedDict supports two additional equivalent forms:: 134 | 135 | Point2D = TypedDict('Point2D', x=int, y=int, label=str) 136 | Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) 137 | 138 | By default, all keys must be present in a TypedDict. It is possible 139 | to override this by specifying totality. 140 | Usage:: 141 | 142 | class point2D(TypedDict, total=False): 143 | x: int 144 | y: int 145 | 146 | This means that a point2D TypedDict can have any of the keys omitted.A type 147 | checker is only expected to support a literal False or True as the value of 148 | the total argument. True is the default, and makes all items defined in the 149 | class body be required. 150 | 151 | The class syntax is only supported in Python 3.6+, while two other 152 | syntax forms work for Python 2.7 and 3.2+ 153 | """ 154 | if fields is None: 155 | fields = kwargs 156 | elif kwargs: 157 | raise TypeError("TypedDict takes either a dict or keyword arguments," 158 | " but not both") 159 | 160 | ns = {'__annotations__': dict(fields)} 161 | try: 162 | # Setting correct module is necessary to make typed dict classes pickleable. 163 | ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__') 164 | except (AttributeError, ValueError): 165 | pass 166 | 167 | return _TypedDictMeta(typename, (), ns, total=total) 168 | 169 | _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {}) 170 | TypedDict.__mro_entries__ = lambda bases: (_TypedDict,) 171 | 172 | 173 | 174 | T: TypeVar = TypeVar('T') 175 | 176 | class Test(TypedDict): 177 | value: int 178 | 179 | class Test2(TypedDict, Generic[T]): 180 | val: Test[T] 181 | 182 | -------------------------------------------------------------------------------- /tests_grammar/05_test_Enums.ini: -------------------------------------------------------------------------------- 1 | 2 | [match:enum] 3 | M1: '''export enum FoldingRangeKind { 4 | /** 5 | * Folding range for a comment 6 | */ 7 | Comment = 'comment', 8 | /** 9 | * Folding range for a imports or includes 10 | */ 11 | Imports = 'imports', 12 | /** 13 | * Folding range for a region (e.g. `#region`) 14 | */ 15 | Region = 'region' 16 | }''' 17 | 18 | M2: '''export enum PluginOutputType { 19 | path = 'path', 20 | }''' 21 | 22 | M3: '''export enum PluginOutputType { 23 | 'path', 24 | }''' 25 | 26 | M4: '''export enum MilkyWay { 27 | earth, moon, stars, 28 | }''' 29 | 30 | M5: '''export enum MilkyWay { 31 | 'earth', 'moon', 'stars', 32 | }''' 33 | 34 | [ast:enum] 35 | 36 | [fail:enum] 37 | 38 | 39 | [match:item] 40 | 41 | [ast:item] 42 | 43 | [fail:item] 44 | 45 | [match:document] 46 | M1: '''export namespace ErrorCodes { 47 | // Defined by JSON-RPC 48 | export const ParseError: integer = -32700; 49 | export const InvalidRequest: integer = -32600; 50 | export const MethodNotFound: integer = -32601; 51 | export const InvalidParams: integer = -32602; 52 | export const InternalError: integer = -32603; 53 | 54 | /** 55 | * This is the start range of JSON-RPC reserved error codes. 56 | * It doesn't denote a real error code. No LSP error codes should 57 | * be defined between the start and end range. For backwards 58 | * compatibility the `ServerNotInitialized` and the `UnknownErrorCode` 59 | * are left in the range. 60 | * 61 | * @since 3.16.0 62 | */ 63 | export const jsonrpcReservedErrorRangeStart: integer = -32099; 64 | /** @deprecated use jsonrpcReservedErrorRangeStart */ 65 | export const serverErrorStart: integer = jsonrpcReservedErrorRangeStart; 66 | 67 | /** 68 | * Error code indicating that a server received a notification or 69 | * request before the server received the `initialize` request. 70 | */ 71 | export const ServerNotInitialized: integer = -32002; 72 | export const UnknownErrorCode: integer = -32001; 73 | 74 | /** 75 | * This is the end range of JSON-RPC reserved error codes. 76 | * It doesn't denote a real error code. 77 | * 78 | * @since 3.16.0 79 | */ 80 | export const jsonrpcReservedErrorRangeEnd = -32000; 81 | /** @deprecated use jsonrpcReservedErrorRangeEnd */ 82 | export const serverErrorEnd: integer = jsonrpcReservedErrorRangeEnd; 83 | 84 | /** 85 | * This is the start range of LSP reserved error codes. 86 | * It doesn't denote a real error code. 87 | * 88 | * @since 3.16.0 89 | */ 90 | export const lspReservedErrorRangeStart: integer = -32899; 91 | 92 | /** 93 | * A request failed but it was syntactically correct, e.g the 94 | * method name was known and the parameters were valid. The error 95 | * message should contain human readable information about why 96 | * the request failed. 97 | * 98 | * @since 3.17.0 99 | */ 100 | export const RequestFailed: integer = -32803; 101 | 102 | /** 103 | * The server cancelled the request. This error code should 104 | * only be used for requests that explicitly support being 105 | * server cancellable. 106 | * 107 | * @since 3.17.0 108 | */ 109 | export const ServerCancelled: integer = -32802; 110 | 111 | /** 112 | * The server detected that the content of a document got 113 | * modified outside normal conditions. A server should 114 | * NOT send this error code if it detects a content change 115 | * in its unprocessed messages. The result even computed 116 | * on an older state might still be useful for the client. 117 | * 118 | * If a client decides that a result is not of any use anymore 119 | * the client should cancel the request. 120 | */ 121 | export const ContentModified: integer = -32801; 122 | 123 | /** 124 | * The client has canceled a request and a server has detected 125 | * the cancel. 126 | */ 127 | export const RequestCancelled: integer = -32800; 128 | 129 | /** 130 | * This is the end range of LSP reserved error codes. 131 | * It doesn't denote a real error code. 132 | * 133 | * @since 3.16.0 134 | */ 135 | export const lspReservedErrorRangeEnd: integer = -32800; 136 | }''' 137 | 138 | [py:document] 139 | M1: '''class ErrorCodes(IntEnum): 140 | 141 | # Defined by JSON-RPC 142 | ParseError = -32700 143 | InvalidRequest = -32600 144 | MethodNotFound = -32601 145 | InvalidParams = -32602 146 | InternalError = -32603 147 | 148 | # This is the start range of JSON-RPC reserved error codes. 149 | # It doesnʼt denote a real error code. No LSP error codes should 150 | # be defined between the start and end range. For backwards 151 | # compatibility the `ServerNotInitialized` and the `UnknownErrorCode` 152 | # are left in the range. 153 | 154 | # @since 3.16.0 155 | jsonrpcReservedErrorRangeStart = -32099 156 | 157 | # @deprecated use jsonrpcReservedErrorRangeStart 158 | serverErrorStart = jsonrpcReservedErrorRangeStart 159 | 160 | # Error code indicating that a server received a notification or 161 | # request before the server received the `initialize` request. 162 | ServerNotInitialized = -32002 163 | UnknownErrorCode = -32001 164 | 165 | # This is the end range of JSON-RPC reserved error codes. 166 | # It doesnʼt denote a real error code. 167 | 168 | # @since 3.16.0 169 | jsonrpcReservedErrorRangeEnd = -32000 170 | 171 | # @deprecated use jsonrpcReservedErrorRangeEnd 172 | serverErrorEnd = jsonrpcReservedErrorRangeEnd 173 | 174 | # This is the start range of LSP reserved error codes. 175 | # It doesnʼt denote a real error code. 176 | 177 | # @since 3.16.0 178 | lspReservedErrorRangeStart = -32899 179 | 180 | # A request failed but it was syntactically correct, e.g the 181 | # method name was known and the parameters were valid. The error 182 | # message should contain human readable information about why 183 | # the request failed. 184 | 185 | # @since 3.17.0 186 | RequestFailed = -32803 187 | 188 | # The server cancelled the request. This error code should 189 | # only be used for requests that explicitly support being 190 | # server cancellable. 191 | 192 | # @since 3.17.0 193 | ServerCancelled = -32802 194 | 195 | # The server detected that the content of a document got 196 | # modified outside normal conditions. A server should 197 | # NOT send this error code if it detects a content change 198 | # in its unprocessed messages. The result even computed 199 | # on an older state might still be useful for the client. 200 | 201 | # If a client decides that a result is not of any use anymore 202 | # the client should cancel the request. 203 | ContentModified = -32801 204 | 205 | # The client has canceled a request and a server has detected 206 | # the cancel. 207 | RequestCancelled = -32800 208 | 209 | # This is the end range of LSP reserved error codes. 210 | # It doesnʼt denote a real error code. 211 | 212 | # @since 3.16.0 213 | lspReservedErrorRangeEnd = -32800''' -------------------------------------------------------------------------------- /tests_grammar/10_test_Typescript_Document.ini: -------------------------------------------------------------------------------- 1 | 2 | [match:document] 3 | M1*: """ 4 | 5 | /** 6 | * Defines an integer number in the range of -2^31 to 2^31 - 1. 7 | */ 8 | export type integer = number; 9 | 10 | /** 11 | * Defines an unsigned integer number in the range of 0 to 2^31 - 1. 12 | */ 13 | export type uinteger = number; 14 | 15 | { 16 | start: { line: 5, character: 23 }, 17 | end : { line: 6, character: 0 } 18 | } 19 | 20 | """ 21 | 22 | M2: """ 23 | type DocumentUri = string; 24 | 25 | type URI = string; 26 | 27 | /** 28 | * Client capabilities specific to regular expressions. 29 | */ 30 | export interface RegularExpressionsClientCapabilities { 31 | /** 32 | * The engine's name. 33 | */ 34 | engine: string; 35 | 36 | /** 37 | * The engine's version. 38 | */ 39 | version?: string; 40 | } 41 | 42 | export const EOL: string[] = ['\n', '\r\n', '\r']; 43 | 44 | interface Position { 45 | /** 46 | * Line position in a document (zero-based). 47 | */ 48 | line: uinteger; 49 | 50 | /** 51 | * Character offset on a line in a document (zero-based). Assuming that 52 | * the line is represented as a string, the `character` value represents 53 | * the gap between the `character` and `character + 1`. 54 | * 55 | * If the character value is greater than the line length it defaults back 56 | * to the line length. 57 | */ 58 | character: uinteger; 59 | } 60 | 61 | { 62 | start: { line: 5, character: 23 }, 63 | end : { line: 6, character: 0 } 64 | } 65 | 66 | interface Range { 67 | /** 68 | * The range's start position. 69 | */ 70 | start: Position; 71 | 72 | /** 73 | * The range's end position. 74 | */ 75 | end: Position; 76 | } 77 | """ 78 | M4: """textDocument.codeAction.resolveSupport = { properties: ['edit'] };""" 79 | M5: '''{ line: 2, startChar: 5, length: 3, tokenType: "property", 80 | tokenModifiers: ["private", "static"] 81 | }, 82 | { line: 2, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }, 83 | { line: 5, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] } 84 | 85 | { 86 | tokenTypes: ['property', 'type', 'class'], 87 | tokenModifiers: ['private', 'static'] 88 | } 89 | 90 | { line: 2, startChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 }, 91 | { line: 2, startChar: 10, length: 4, tokenType: 1, tokenModifiers: 0 }, 92 | { line: 5, startChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 } 93 | 94 | { deltaLine: 2, deltaStartChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 }, 95 | { deltaLine: 0, deltaStartChar: 5, length: 4, tokenType: 1, tokenModifiers: 0 }, 96 | { deltaLine: 3, deltaStartChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 } 97 | 98 | // 1st token, 2nd token, 3rd token 99 | [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] 100 | 101 | { line: 3, startChar: 5, length: 3, tokenType: "property", 102 | tokenModifiers: ["private", "static"] 103 | }, 104 | { line: 3, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }, 105 | { line: 6, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] } 106 | 107 | // 1st token, 2nd token, 3rd token 108 | [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0]''' 109 | M6: """interface InitializeParams extends WorkDoneProgressParams { 110 | processId: integer | null; 111 | clientInfo?: { 112 | name: string; 113 | version?: string; 114 | }; 115 | locale?: string; 116 | rootPath?: string | null; 117 | rootUri: DocumentUri | null; 118 | initializationOptions?: any; 119 | capabilities: ClientCapabilities; 120 | trace?: TraceValue; 121 | workspaceFolders?: WorkspaceFolder[] | null; 122 | }""" 123 | M7: """ 124 | interface Position { 125 | line: uinteger; 126 | character: uinteger; 127 | } 128 | interface Range { 129 | start: Position; 130 | end: Position; 131 | } 132 | interface TextEdit { 133 | range: Range; 134 | newText: string; 135 | } 136 | export type ChangeAnnotationIdentifier = string; 137 | /* export interface AnnotatedTextEdit extends TextEdit { 138 | annotationId: ChangeAnnotationIdentifier; 139 | }*/ 140 | export interface TextDocumentEdit { 141 | textDocument: string; 142 | edits: (TextEdit | AnnotatedTextEdit)[]; 143 | }""" 144 | M8: """import {ChangeInfo, CommentRange} from './rest-api'; 145 | export declare interface ChecksPluginApi { 146 | register(provider: ChecksProvider, config?: ChecksApiConfig): void; 147 | announceUpdate(): void; 148 | updateResult(run: CheckRun, result: CheckResult): void; 149 | }""" 150 | M9: """import {ChartArea} from './geometric.js'; 151 | 152 | export type LayoutPosition = 'left' | 'top' | 'right' | 'bottom' | 'center' | 'chartArea' | {[scaleId: string]: number}; 153 | 154 | export interface LayoutItem { 155 | position: LayoutPosition; 156 | weight: number; 157 | fullSize: boolean; 158 | width: number; 159 | height: number; 160 | left: number; 161 | top: number; 162 | right: number; 163 | bottom: number; 164 | 165 | beforeLayout?(): void; 166 | draw(chartArea: ChartArea): void; 167 | getPadding?(): ChartArea; 168 | isHorizontal(): boolean; 169 | update(width: number, height: number, margins?: ChartArea): void; 170 | }""" 171 | 172 | M10: """export namespace SymbolKind { 173 | export const File = 1; 174 | export const Module = 2; 175 | export const Namespace = 3; 176 | export const Package = 4; 177 | export const Class = 5; 178 | export const Method = 6; 179 | export const Property = 7; 180 | export const Field = 8; 181 | export const Constructor = 9; 182 | export const Enum = 10; 183 | export const Interface = 11; 184 | export const Function = 12; 185 | export const Variable = 13; 186 | export const Constant = 14; 187 | export const String = 15; 188 | export const Number = 16; 189 | export const Boolean = 17; 190 | export const Array = 18; 191 | export const Object = 19; 192 | export const Key = 20; 193 | export const Null = 21; 194 | export const EnumMember = 22; 195 | export const Struct = 23; 196 | export const Event = 24; 197 | export const Operator = 25; 198 | export const TypeParameter = 26; 199 | } 200 | 201 | export type SymbolKind = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 202 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26;""" 203 | 204 | M11: """export type PositionEncodingKind = string; 205 | 206 | export namespace PositionEncodingKind { 207 | export const UTF8: PositionEncodingKind = 'utf-8'; 208 | export const UTF16: PositionEncodingKind = 'utf-16'; 209 | export const UTF32: PositionEncodingKind = 'utf-32'; 210 | }""" 211 | 212 | M12: """declare module 'vscode' { 213 | export class CancellationError extends Error { 214 | 215 | /** 216 | * Creates a new cancellation error. 217 | */ 218 | constructor(); 219 | }}""" 220 | 221 | [ast:document] 222 | 223 | [fail:document] 224 | 225 | [py:document] 226 | M10: """class SymbolKind(IntEnum): 227 | File = 1 228 | Module = 2 229 | Namespace = 3 230 | Package = 4 231 | Class = 5 232 | Method = 6 233 | Property = 7 234 | Field = 8 235 | Constructor = 9 236 | Enum = 10 237 | Interface = 11 238 | Function = 12 239 | Variable = 13 240 | Constant = 14 241 | String = 15 242 | Number = 16 243 | Boolean = 17 244 | Array = 18 245 | Object = 19 246 | Key = 20 247 | Null = 21 248 | EnumMember = 22 249 | Struct = 23 250 | Event = 24 251 | Operator = 25 252 | TypeParameter = 26 253 | 254 | # commented out, because there is already an enumeration with the same name 255 | # SymbolKind = int""" 256 | 257 | M11: '''PositionEncodingKind = str 258 | 259 | class PositionEncodingKind(Enum): 260 | UTF8 = "utf-8" 261 | UTF16 = "utf-16" 262 | UTF32 = "utf-32"''' 263 | 264 | M12: ''' 265 | class CancellationError(Exception): 266 | pass''' 267 | -------------------------------------------------------------------------------- /tests_grammar/06_test_Namespaces.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.UsePostponedEvaluation = False 3 | ts2python.AllowReadOnly = True 4 | ts2python.KeepMultilineComments = False 5 | 6 | [match:virtual_enum] 7 | M1: """export namespace ErrorCodes { 8 | // Defined by JSON RPC 9 | export const ParseError: integer = -32700; 10 | export const InvalidRequest: integer = -32600; 11 | export const MethodNotFound: integer = -32601; 12 | export const InvalidParams: integer = -32602; 13 | export const InternalError: integer = -32603; 14 | export const jsonrpcReservedErrorRangeStart: integer = -32099; 15 | export const serverErrorStart: integer = jsonrpcReservedErrorRangeStart; 16 | export const ServerNotInitialized: integer = -32002; 17 | export const UnknownErrorCode: integer = -32001; 18 | export const jsonrpcReservedErrorRangeEnd = -32000; 19 | export const serverErrorEnd: integer = jsonrpcReservedErrorRangeEnd; 20 | export const lspReservedErrorRangeStart: integer = -32899; 21 | export const ContentModified: integer = -32801; 22 | export const RequestCancelled: integer = -32800; 23 | export const lspReservedErrorRangeEnd: integer = -32800; 24 | }""" 25 | M2: """export namespace DiagnosticSeverity { 26 | /** 27 | * Reports an error. 28 | */ 29 | export const Error: 1 = 1; 30 | /** 31 | * Reports a warning. 32 | */ 33 | export const Warning: 2 = 2; 34 | /** 35 | * Reports an information. 36 | */ 37 | export const Information: 3 = 3; 38 | /** 39 | * Reports a hint. 40 | */ 41 | export const Hint: 4 = 4; 42 | }""" 43 | M3: """export namespace TokenFormat { 44 | export const Relative: 'relative' = 'relative'; 45 | }""" 46 | M4: """export namespace TextDocumentSyncKind { 47 | export const None = 0; 48 | export const Full = 1; 49 | export const Incremental = 2; 50 | }""" 51 | 52 | [match:namespace] 53 | M1: """export namespace tasks { 54 | export function registerTaskProvider(type: string, provider: TaskProvider): Disposable; 55 | export function fetchTasks(filter?: TaskFilter): Thenable; 56 | export function executeTask(task: Task): Thenable; 57 | export const taskExecutions: readonly TaskExecution[]; 58 | export const onDidStartTask: Event; 59 | export const onDidEndTask: Event; 60 | export const onDidStartTaskProcess: Event; 61 | export const onDidEndTaskProcess: Event; 62 | }""" 63 | M2: """export namespace ErrorCodes { 64 | // Defined by JSON-RPC 65 | export const ParseError: integer = -32700; 66 | export const InvalidRequest: integer = -32600; 67 | export const MethodNotFound: integer = -32601; 68 | export const InvalidParams: integer = -32602; 69 | export const InternalError: integer = -32603; 70 | 71 | /** 72 | * This is the start range of JSON-RPC reserved error codes. 73 | * It doesn't denote a real error code. No LSP error codes should 74 | * be defined between the start and end range. For backwards 75 | * compatibility the `ServerNotInitialized` and the `UnknownErrorCode` 76 | * are left in the range. 77 | * 78 | * @since 3.16.0 79 | */ 80 | export const jsonrpcReservedErrorRangeStart: integer = -32099; 81 | /** @deprecated use jsonrpcReservedErrorRangeStart */ 82 | export const serverErrorStart: integer = jsonrpcReservedErrorRangeStart; 83 | 84 | /** 85 | * Error code indicating that a server received a notification or 86 | * request before the server has received the `initialize` request. 87 | */ 88 | export const ServerNotInitialized: integer = -32002; 89 | export const UnknownErrorCode: integer = -32001; 90 | 91 | /** 92 | * This is the end range of JSON-RPC reserved error codes. 93 | * It doesn't denote a real error code. 94 | * 95 | * @since 3.16.0 96 | */ 97 | export const jsonrpcReservedErrorRangeEnd = -32000; 98 | /** @deprecated use jsonrpcReservedErrorRangeEnd */ 99 | export const serverErrorEnd: integer = jsonrpcReservedErrorRangeEnd; 100 | 101 | /** 102 | * This is the start range of LSP reserved error codes. 103 | * It doesn't denote a real error code. 104 | * 105 | * @since 3.16.0 106 | */ 107 | export const lspReservedErrorRangeStart: integer = -32899; 108 | 109 | /** 110 | * A request failed but it was syntactically correct, e.g the 111 | * method name was known and the parameters were valid. The error 112 | * message should contain human readable information about why 113 | * the request failed. 114 | * 115 | * @since 3.17.0 116 | */ 117 | export const RequestFailed: integer = -32803; 118 | 119 | /** 120 | * The server cancelled the request. This error code should 121 | * only be used for requests that explicitly support being 122 | * server cancellable. 123 | * 124 | * @since 3.17.0 125 | */ 126 | export const ServerCancelled: integer = -32802; 127 | 128 | /** 129 | * The server detected that the content of a document got 130 | * modified outside normal conditions. A server should 131 | * NOT send this error code if it detects a content change 132 | * in its unprocessed messages. The result even computed 133 | * on an older state might still be useful for the client. 134 | * 135 | * If a client decides that a result is not of any use anymore 136 | * the client should cancel the request. 137 | */ 138 | export const ContentModified: integer = -32801; 139 | 140 | /** 141 | * The client has canceled a request and a server has detected 142 | * the cancel. 143 | */ 144 | export const RequestCancelled: integer = -32800; 145 | 146 | /** 147 | * This is the end range of LSP reserved error codes. 148 | * It doesn't denote a real error code. 149 | * 150 | * @since 3.16.0 151 | */ 152 | export const lspReservedErrorRangeEnd: integer = -32800; 153 | }""" 154 | 155 | [py:namespace] 156 | M1: """ 157 | class tasks: 158 | def registerTaskProvider(type: str, provider: 'TaskProvider') -> 'Disposable': 159 | pass 160 | 161 | def fetchTasks(filter: Optional['TaskFilter'] = None) -> 'Thenable[List[Task]]': 162 | pass 163 | 164 | def executeTask(task: 'Task') -> 'Thenable[TaskExecution]': 165 | pass 166 | taskExecutions: ReadOnly[List['TaskExecution']] 167 | onDidStartTask: 'Event[TaskStartEvent]' 168 | onDidEndTask: 'Event[TaskEndEvent]' 169 | onDidStartTaskProcess: 'Event[TaskProcessStartEvent]' 170 | onDidEndTaskProcess: 'Event[TaskProcessEndEvent]' 171 | """ 172 | M2: """ 173 | class ErrorCodes: 174 | ParseError: int = -32700 175 | InvalidRequest: int = -32600 176 | MethodNotFound: int = -32601 177 | InvalidParams: int = -32602 178 | InternalError: int = -32603 179 | jsonrpcReservedErrorRangeStart: int = -32099 180 | serverErrorStart: int = jsonrpcReservedErrorRangeStart 181 | ServerNotInitialized: int = -32002 182 | UnknownErrorCode: int = -32001 183 | jsonrpcReservedErrorRangeEnd: Any = -32000 184 | serverErrorEnd: int = jsonrpcReservedErrorRangeEnd 185 | lspReservedErrorRangeStart: int = -32899 186 | RequestFailed: int = -32803 187 | ServerCancelled: int = -32802 188 | ContentModified: int = -32801 189 | RequestCancelled: int = -32800 190 | lspReservedErrorRangeEnd: int = -32800 191 | """ 192 | 193 | [match:declarations_block] 194 | M1*: """{ 195 | /** 196 | * The name of the function to which this breakpoint is attached. 197 | */ 198 | readonly functionName: string; 199 | 200 | /** 201 | * Create a new function breakpoint. 202 | */ 203 | constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string); 204 | }""" 205 | -------------------------------------------------------------------------------- /tests/runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # stolen from DHParser.testing 3 | 4 | import collections 5 | import concurrent.futures 6 | import inspect 7 | import os 8 | import sys 9 | 10 | 11 | def run_tests_in_class(cls_name, namespace, methods=()): 12 | """ 13 | Runs tests in test-class `test` in the given namespace. 14 | 15 | """ 16 | def instantiate(cls, nspace): 17 | """Instantiates class name `cls` within name-space `nspace` and 18 | returns the instance.""" 19 | exec("instance = " + cls + "()", nspace) 20 | instance = nspace["instance"] 21 | setup = instance.setup_class if "setup_class" in dir(instance) \ 22 | else (instance.setup if "setup" in dir(instance) else lambda : 0) 23 | teardown = instance.teardown_class if "tear_down_class" in dir(instance) \ 24 | else (instance.teardown if "teardown" in dir(instance) else lambda : 0) 25 | return instance, setup, teardown 26 | 27 | obj = None 28 | if methods: 29 | obj, setup, teardown = instantiate(cls_name, namespace) 30 | for name in methods: 31 | func = obj.__getattribute__(name) 32 | if callable(func): 33 | print("Running " + cls_name + "." + name) 34 | setup(); func(); teardown() 35 | # exec('obj.' + name + '()') 36 | else: 37 | obj, setup, teardown = instantiate(cls_name, namespace) 38 | for name in dir(obj): 39 | if name.lower().startswith("test"): 40 | func = obj.__getattribute__(name) 41 | if callable(func): 42 | print("Running " + cls_name + "." + name) 43 | setup(); func(); teardown() 44 | 45 | 46 | def run_test_function(func_name, namespace): 47 | """ 48 | Run the test-function `test` in the given namespace. 49 | """ 50 | print("Running test-function: " + func_name) 51 | exec(func_name + '()', namespace) 52 | 53 | 54 | def runner(tests, namespace, profile=False): 55 | """ 56 | Runs all or some selected Python unit tests found in the 57 | namespace. To run all tests in a module, call 58 | ``runner("", globals())`` from within that module. 59 | 60 | Unit-Tests are either classes, the name of which starts with 61 | "Test" and methods, the name of which starts with "test" contained 62 | in such classes or functions, the name of which starts with "test". 63 | 64 | if `tests` is either the empty string or an empty sequence, runner 65 | checks sys.argv for specified tests. In case sys.argv[0] (i.e. the 66 | script's file name) starts with 'test' any argument in sys.argv[1:] 67 | (i.e. the rest of the command line) that starts with 'test' or 68 | 'Test' is considered the name of a test function or test method 69 | (of a test-class) that shall be run. Test-Methods are specified in 70 | the form: class_name.method.name e.g. "TestServer.test_connection". 71 | 72 | :param tests: String or list of strings with the names of tests 73 | to run. If empty, runner searches by itself all objects the 74 | of which starts with 'test' and runs it (if its a function) 75 | or all of its methods that start with "test" if its a class 76 | plus the "setup" and "teardown" methods if they exist. 77 | 78 | :param namespace: The namespace for running the test, usually 79 | ``globals()`` should be used. 80 | 81 | :param profile: If True, the tests will be run with the profiler on. 82 | results will be displayed after the test-results. Profiling will 83 | also be turned on, if the parameter `--profile` has been provided 84 | on the command line. 85 | """ 86 | test_classes = collections.OrderedDict() 87 | test_functions = [] 88 | 89 | if tests: 90 | if isinstance(tests, str): 91 | tests = tests.split(' ') 92 | assert all(test.lower().startswith('test') for test in tests) 93 | else: 94 | tests = [] 95 | if os.path.basename(sys.argv[0]).lower().startswith('test'): 96 | tests = [name for name in sys.argv[1:] 97 | if os.path.basename(name.lower()).startswith('test')] 98 | if not tests: 99 | tests = [name for name in namespace.keys() if name.lower().startswith('test')] 100 | 101 | for name in tests: 102 | func_or_class, method = (name.split('.') + [''])[:2] 103 | if inspect.isclass(namespace[func_or_class]): 104 | if func_or_class not in test_classes: 105 | test_classes[func_or_class] = [] 106 | if method: 107 | test_classes[func_or_class].append(method) 108 | elif inspect.isfunction(namespace[name]): 109 | test_functions.append(name) 110 | 111 | profile = profile or '--profile' in sys.argv 112 | if profile: 113 | import cProfile, pstats 114 | pr = cProfile.Profile() 115 | pr.enable() 116 | 117 | for cls_name, methods in test_classes.items(): 118 | run_tests_in_class(cls_name, namespace, methods) 119 | 120 | for test in test_functions: 121 | run_test_function(test, namespace) 122 | 123 | if profile: 124 | pr.disable() 125 | st = pstats.Stats(pr) 126 | st.strip_dirs() 127 | st.sort_stats('time').print_stats(50) 128 | 129 | 130 | def run_file(fname): 131 | dirname, fname = os.path.split(fname) 132 | if fname.lower().startswith('test_') and fname.endswith('.py'): 133 | print("RUNNING " + fname + " with: " + sys.version) 134 | # print('\nRUNNING UNIT TESTS IN: ' + fname) 135 | save = os.getcwd() 136 | os.chdir(dirname) 137 | exec('import ' + fname[:-3]) 138 | runner('', eval(fname[:-3]).__dict__) 139 | os.chdir(save) 140 | 141 | 142 | class SingleThreadExecutor(concurrent.futures.Executor): 143 | """SingleThreadExecutor is a replacement for 144 | concurrent.future.ProcessPoolExecutor and 145 | concurrent.future.ThreadPoolExecutor that executes any submitted 146 | task immediately in the submitting thread. This helps to avoid 147 | writing extra code for the case that multithreading or 148 | multiprocesssing has been turned off in the configuration. To do 149 | so is helpful for debugging. 150 | 151 | It is not recommended to use this in asynchronous code or code that 152 | relies on the submit() or map()-method of executors to return quickly. 153 | """ 154 | def submit(self, fn, *args, **kwargs): 155 | future = concurrent.futures.Future() 156 | try: 157 | result = fn(*args, **kwargs) 158 | future.set_result(result) 159 | except BaseException as e: 160 | future.set_exception(e) 161 | return future 162 | 163 | 164 | def run_path(path): 165 | """Runs all unit tests in `path`""" 166 | if os.path.isdir(path): 167 | sys.path.append(path) 168 | files = os.listdir(path) 169 | results = [] 170 | with SingleThreadExecutor() as pool: 171 | for f in files: 172 | if f.find('test') >= 0 and f[-3:] == '.py': 173 | results.append(pool.submit(run_file, os.path.join(path, f))) 174 | # run_file(f) # for testing! 175 | assert results, f"No tests found in directory {os.path.abspath(path)}" 176 | concurrent.futures.wait(results) 177 | for r in results: 178 | try: 179 | _ = r.result() 180 | except AssertionError as failure: 181 | print(failure) 182 | 183 | else: 184 | path, fname = os.path.split(path) 185 | sys.path.append(path) 186 | run_file(fname) 187 | sys.path.pop() 188 | 189 | 190 | def run_doctests(): 191 | import doctest 192 | scriptdir = os.path.dirname(os.path.abspath(__file__)) 193 | ts2python_path = os.path.abspath(os.path.join(scriptdir, '..', 'ts2python')) 194 | sys.path.append(ts2python_path) 195 | import json_validation 196 | print('Running doctests of ts2python.json_validation') 197 | doctest.testmod(json_validation) 198 | import typeddict_shim 199 | print('Running doctests of ts2python.typeddict_shim') 200 | doctest.testmod(typeddict_shim) 201 | print('Running doctests of docs/Validation.rst') 202 | doctest.testfile(os.path.join('..', 'docs', 'Validation.rst')) 203 | 204 | 205 | if __name__ == "__main__": 206 | path = '.' 207 | if len(sys.argv) > 1: 208 | path = sys.argv[1] 209 | if os.path.isdir(path): 210 | run_path(path) 211 | else: 212 | run_file(path) 213 | run_doctests() 214 | print() 215 | 216 | -------------------------------------------------------------------------------- /ts2python.ebnf: -------------------------------------------------------------------------------- 1 | # a grammar for a subset of the Typescript programming language 2 | 3 | # for Typescript see: https://www.typescriptlang.org/docs/ 4 | # for examples of typescript-interfaces 5 | # see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/ 6 | 7 | ####################################################################### 8 | # 9 | # EBNF-Directives 10 | # 11 | ####################################################################### 12 | 13 | @ optimizations = all 14 | @ whitespace = /\s*/ # insignificant whitespace, includes linefeeds 15 | @ literalws = right # literals have implicit whitespace on the right hand side 16 | @ comment = /(?:\/\/.*)\n?|(?:\/\*(?:.|\n)*?\*\/) *\n?/ # /* ... */ or // to EOL 17 | @ ignorecase = False # literals and regular expressions are case-sensitive 18 | @ reduction = merge_treetops # anonymous nodes are being reduced where possible 19 | @ disposable = INT, NEG, FRAC, DOT, EXP, EOF, 20 | _array_ellipsis, _top_level_assignment, _top_level_literal, 21 | _quoted_identifier, _namespace, _part, _reserved, _keyword 22 | @ drop = whitespace, no_comments, strings, EOF, 23 | _array_ellipsis, _top_level_assignment, _top_level_literal, 24 | _reserved, _keyword 25 | 26 | 27 | ####################################################################### 28 | # 29 | #: Typescript Document 30 | # 31 | ####################################################################### 32 | 33 | root = document EOF 34 | document = ~ { interface | type_alias | _namespace | enum | const | module 35 | | _top_level_assignment | _array_ellipsis | _top_level_literal 36 | | Import [";"] | function [";"] | declaration [";"] } 37 | 38 | @interface_resume = /(?=export|$)/ 39 | @type_alias_resume = /(?=export|$)/ 40 | @enum_resume = /(?=export|$)/ 41 | @const_resume = /(?=export|$)/ 42 | @declaration_resume = /(?=export|$)/ 43 | @_top_level_assignment_resume = /(?=export|$)/ 44 | @_top_level_literal_resume = /(?=export|$)/ 45 | @module_resume = /(?=export|$)/ 46 | 47 | module = "declare" "module" _quoted_identifier "{" document "}" 48 | 49 | 50 | ####################################################################### 51 | # 52 | #: Imports 53 | # 54 | ####################################################################### 55 | 56 | Import = "import" [(wildcard | importList ) "from"] string 57 | importList = (symList | identifier) { "," (symList | identifier) } 58 | symList = "{" symbol { "," symbol } "}" 59 | symbol = ["type"] identifier [ "as" alias ] 60 | wildcard = "*" "as" alias 61 | alias = identifier 62 | 63 | ####################################################################### 64 | # 65 | #: Interfaces 66 | # 67 | ####################################################################### 68 | 69 | interface = ["export"] ["declare"] ("interface"|"class") §identifier [type_parameters] 70 | [extends] declarations_block [";"] 71 | extends = ("extends" | "implements") 72 | (generic_type|type_name) { "," (generic_type|type_name)} 73 | 74 | declarations_block = "{" [ (function|declaration) { [";"|","] (function|declaration) } [";"|","] ] 75 | [map_signature [";"|","] ] "}" 76 | declaration = ["export"] qualifiers ["let"|"var"] !_keyword identifier [optional] !`(` [":" §types] 77 | 78 | 79 | # @function_resume = /(?=;)/ 80 | 81 | function = [["export"] qualifiers 82 | ["function"] !_keyword identifier [optional] [type_parameters]] 83 | "(" §[arg_list] ")" [":" §types] 84 | | special 85 | arg_list = (argument { "," argument } ["," arg_tail] | arg_tail) [","] 86 | arg_tail = "..." identifier [":" §(array_of | generic_type)] 87 | argument = identifier [optional] [":" §types] # same as declaration ? 88 | special = "[" name "]" "(" §[arg_list] ")" [":" §types] 89 | 90 | _keyword = (`readonly` | `function` | `const` | `public` | `private` | `protected`) !/\w/ 91 | 92 | qualifiers = [readonly] ° [static] ° ['public'] ° ['protected'] ° ['private'] 93 | readonly = "readonly" 94 | static = "static" 95 | 96 | optional = "?" 97 | 98 | ####################################################################### 99 | # 100 | #: Types 101 | # 102 | ####################################################################### 103 | 104 | type_alias = ["export"] "type" §identifier [type_parameters] "=" types [";"] 105 | 106 | types = ["|"] (intersection | type) { "|" (intersection | type) } 107 | intersection = type { "&" §type }+ 108 | type = [readonly] 109 | ( array_of | basic_type | generic_type | indexed_type 110 | | type_name !`(` | "(" types ")" | mapped_type 111 | | declarations_block | type_tuple | declarations_tuple 112 | | literal | func_type ) 113 | 114 | generic_type = type_name type_parameters 115 | type_parameters = "<" §parameter_types { "," parameter_types } ">" 116 | parameter_types = ["|"] parameter_type { "|" parameter_type } 117 | parameter_type = [readonly] 118 | ( array_of | basic_type | generic_type # actually, only a subset of array_of 119 | | indexed_type | type_name [extends_type] [equals_type] 120 | | declarations_block | type_tuple | declarations_tuple ) 121 | extends_type = "extends" [keyof] (basic_type | type_name | mapped_type ) 122 | equals_type = "=" (basic_type|type_name) 123 | 124 | array_of = array_types "[]" 125 | array_types = array_type 126 | array_type = basic_type | generic_type | type_name | "(" types ")" 127 | | type_tuple | declarations_block 128 | 129 | type_name = name 130 | type_tuple = "[" types {"," types} "]" 131 | declarations_tuple = "[" declaration { "," declaration } "]" 132 | 133 | mapped_type = "{" map_signature [";"] "}" 134 | map_signature = index_signature ":" types 135 | index_signature = [readonly] 136 | "[" identifier (":" | ["in"] keyof | "in") type "]" 137 | [optional] 138 | keyof = "keyof" 139 | 140 | indexed_type = type_name "[" (type_name | literal) "]" 141 | 142 | func_type = ["new"] "(" [arg_list] ")" "=>" types 143 | 144 | ####################################################################### 145 | # 146 | #: Namespaces 147 | # 148 | ####################################################################### 149 | 150 | _namespace = virtual_enum | namespace 151 | 152 | virtual_enum = ["export"] "namespace" identifier "{" 153 | { interface | type_alias | enum | const 154 | | declaration [";"] } "}" 155 | 156 | namespace = ["export"] "namespace" §identifier "{" 157 | { interface | type_alias | enum | const 158 | | declaration [";"] | function [";"] } "}" 159 | 160 | ####################################################################### 161 | # 162 | #: Enums 163 | # 164 | ####################################################################### 165 | 166 | enum = ["export"] "enum" identifier §"{" item { "," item } [","] "}" 167 | item = _quoted_identifier ["=" literal] 168 | 169 | ####################################################################### 170 | # 171 | #: Consts 172 | # 173 | ####################################################################### 174 | 175 | const = ["export"] "const" §declaration ["=" (literal | identifier)] [";"] 176 | _top_level_assignment = assignment 177 | assignment = variable "=" (literal | variable) [";"] # no expressions, yet 178 | 179 | ####################################################################### 180 | # 181 | #: literals 182 | # 183 | ####################################################################### 184 | 185 | _array_ellipsis = literal { "," literal } 186 | 187 | _top_level_literal = literal 188 | literal = integer | number | boolean | string | array | object 189 | integer = INT !/[.Ee]/ ~ 190 | number = INT FRAC EXP ~ 191 | boolean = (`true` | `false`) ~ 192 | string = /"[^"\n]*"/~ | /'[^'\n]*'/~ 193 | array = "[" [ literal { "," literal } ] "]" 194 | object = "{" [ association { "," association } ] [","] "}" 195 | association = key §":" literal 196 | key = identifier | '"' identifier '"' 197 | 198 | ####################################################################### 199 | # 200 | #: Keywords 201 | # 202 | ####################################################################### 203 | 204 | basic_type = (`object` | `array` | `string` | `number` | `boolean` | `null` 205 | | `integer` | `uinteger` | `decimal` | `unknown` | `any` | `void`) ~ 206 | 207 | ####################################################################### 208 | # 209 | #: Entities 210 | # 211 | ####################################################################### 212 | 213 | variable = name 214 | _quoted_identifier = identifier | '"' identifier §'"' | "'" identifier §"'" 215 | name = !_reserved _part {`.` _part} ~ 216 | identifier = !_reserved _part ~ 217 | _part = /(?!\d)\w+/ 218 | _reserved = `true` | `false` 219 | INT = [NEG] ( /[1-9][0-9]+/ | /[0-9]/ ) 220 | NEG = `-` 221 | FRAC = [ DOT /[0-9]+/ ] 222 | DOT = `.` 223 | EXP = [ (`E`|`e`) [`+`|`-`] /[0-9]+/ ] 224 | 225 | EOF = !/./ # no more characters ahead, end of file reached 226 | -------------------------------------------------------------------------------- /tests_grammar/11_test_comments.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.RenderAnonymous = "toplevel" 3 | ts2python.UsePostponedEvaluation = True 4 | ts2python.UseTypeUnion = True 5 | ts2python.UseExplicitTypeAlias = True 6 | ts2python.UseTypeParameters = True 7 | ts2python.UseLiteralType = True 8 | ts2python.UseVariadicGenerics = True 9 | ts2python.UseNotRequired = True 10 | ts2python.AllowReadOnly = True 11 | ts2python.AssumeDeferredEvaluation = False 12 | ts2python.KeepMultilineComments = True 13 | 14 | [match:namespace] 15 | M1: """export namespace ErrorCodes { 16 | // Defined by JSON-RPC 17 | export const ParseError: integer = -32700; 18 | export const InvalidRequest: integer = -32600; 19 | export const MethodNotFound: integer = -32601; 20 | export const InvalidParams: integer = -32602; 21 | export const InternalError: integer = -32603; 22 | 23 | /** 24 | * This is the start range of JSON-RPC reserved error codes. 25 | * It doesn't denote a real error code. No LSP error codes should 26 | * be defined between the start and end range. For backwards 27 | * compatibility the `ServerNotInitialized` and the `UnknownErrorCode` 28 | * are left in the range. 29 | * 30 | * @since 3.16.0 31 | */ 32 | export const jsonrpcReservedErrorRangeStart: integer = -32099; 33 | /** @deprecated use jsonrpcReservedErrorRangeStart */ 34 | export const serverErrorStart: integer = jsonrpcReservedErrorRangeStart; 35 | 36 | /** 37 | * Error code indicating that a server received a notification or 38 | * request before the server has received the `initialize` request. 39 | */ 40 | export const ServerNotInitialized: integer = -32002; 41 | export const UnknownErrorCode: integer = -32001; 42 | 43 | /** 44 | * This is the end range of JSON-RPC reserved error codes. 45 | * It doesn't denote a real error code. 46 | * 47 | * @since 3.16.0 48 | */ 49 | export const jsonrpcReservedErrorRangeEnd = -32000; 50 | /** @deprecated use jsonrpcReservedErrorRangeEnd */ 51 | export const serverErrorEnd: integer = jsonrpcReservedErrorRangeEnd; 52 | 53 | /** 54 | * This is the start range of LSP reserved error codes. 55 | * It doesn't denote a real error code. 56 | * 57 | * @since 3.16.0 58 | */ 59 | export const lspReservedErrorRangeStart: integer = -32899; 60 | 61 | /** 62 | * A request failed but it was syntactically correct, e.g the 63 | * method name was known and the parameters were valid. The error 64 | * message should contain human readable information about why 65 | * the request failed. 66 | * 67 | * @since 3.17.0 68 | */ 69 | export const RequestFailed: integer = -32803; 70 | 71 | /** 72 | * The server cancelled the request. This error code should 73 | * only be used for requests that explicitly support being 74 | * server cancellable. 75 | * 76 | * @since 3.17.0 77 | */ 78 | export const ServerCancelled: integer = -32802; 79 | 80 | /** 81 | * The server detected that the content of a document got 82 | * modified outside normal conditions. A server should 83 | * NOT send this error code if it detects a content change 84 | * in its unprocessed messages. The result even computed 85 | * on an older state might still be useful for the client. 86 | * 87 | * If a client decides that a result is not of any use anymore 88 | * the client should cancel the request. 89 | */ 90 | export const ContentModified: integer = -32801; 91 | 92 | /** 93 | * The client has canceled a request and a server has detected 94 | * the cancel. 95 | */ 96 | export const RequestCancelled: integer = -32800; 97 | 98 | /** 99 | * This is the end range of LSP reserved error codes. 100 | * It doesn't denote a real error code. 101 | * 102 | * @since 3.16.0 103 | */ 104 | export const lspReservedErrorRangeEnd: integer = -32800; 105 | }""" 106 | 107 | [py:namespace] 108 | M1: """ 109 | class ErrorCodes: 110 | # Defined by JSON-RPC 111 | ParseError: int = -32700 112 | InvalidRequest: int = -32600 113 | MethodNotFound: int = -32601 114 | InvalidParams: int = -32602 115 | InternalError: int = -32603 116 | 117 | # This is the start range of JSON-RPC reserved error codes. 118 | # It doesnʼt denote a real error code. No LSP error codes should 119 | # be defined between the start and end range. For backwards 120 | # compatibility the `ServerNotInitialized` and the `UnknownErrorCode` 121 | # are left in the range. 122 | 123 | # @since 3.16.0 124 | jsonrpcReservedErrorRangeStart: int = -32099 125 | 126 | # @deprecated use jsonrpcReservedErrorRangeStart 127 | serverErrorStart: int = jsonrpcReservedErrorRangeStart 128 | 129 | # Error code indicating that a server received a notification or 130 | # request before the server has received the `initialize` request. 131 | ServerNotInitialized: int = -32002 132 | UnknownErrorCode: int = -32001 133 | 134 | # This is the end range of JSON-RPC reserved error codes. 135 | # It doesnʼt denote a real error code. 136 | 137 | # @since 3.16.0 138 | jsonrpcReservedErrorRangeEnd: Any = -32000 139 | 140 | # @deprecated use jsonrpcReservedErrorRangeEnd 141 | serverErrorEnd: int = jsonrpcReservedErrorRangeEnd 142 | 143 | # This is the start range of LSP reserved error codes. 144 | # It doesnʼt denote a real error code. 145 | 146 | # @since 3.16.0 147 | lspReservedErrorRangeStart: int = -32899 148 | 149 | # A request failed but it was syntactically correct, e.g the 150 | # method name was known and the parameters were valid. The error 151 | # message should contain human readable information about why 152 | # the request failed. 153 | 154 | # @since 3.17.0 155 | RequestFailed: int = -32803 156 | 157 | # The server cancelled the request. This error code should 158 | # only be used for requests that explicitly support being 159 | # server cancellable. 160 | 161 | # @since 3.17.0 162 | ServerCancelled: int = -32802 163 | 164 | # The server detected that the content of a document got 165 | # modified outside normal conditions. A server should 166 | # NOT send this error code if it detects a content change 167 | # in its unprocessed messages. The result even computed 168 | # on an older state might still be useful for the client. 169 | 170 | # If a client decides that a result is not of any use anymore 171 | # the client should cancel the request. 172 | ContentModified: int = -32801 173 | 174 | # The client has canceled a request and a server has detected 175 | # the cancel. 176 | RequestCancelled: int = -32800 177 | 178 | # This is the end range of LSP reserved error codes. 179 | # It doesnʼt denote a real error code. 180 | 181 | # @since 3.16.0 182 | lspReservedErrorRangeEnd: int = -32800 183 | """ 184 | 185 | [match:document] 186 | M1: """ 187 | type DocumentUri = string; 188 | 189 | type URI = string; 190 | 191 | 192 | /* source file: "types/regexp.md" */ 193 | 194 | /** 195 | * Client capabilities specific to regular expressions. 196 | */ 197 | export interface RegularExpressionsClientCapabilities { 198 | /** 199 | * The engine's name. 200 | */ 201 | engine: string; 202 | 203 | /** 204 | * The engine's version. 205 | */ 206 | version?: string; 207 | } 208 | """ 209 | 210 | M2: """export type TextDocumentContentChangeEvent = { 211 | /** 212 | * The range of the document that changed. 213 | */ 214 | range: Range; 215 | 216 | /** 217 | * The optional length of the range that got replaced. 218 | * 219 | * @deprecated use range instead. 220 | */ 221 | rangeLength?: uinteger; 222 | 223 | /** 224 | * The new text for the provided range. 225 | */ 226 | text: string; 227 | } | { 228 | /** 229 | * The new text of the whole document. 230 | */ 231 | text: string; 232 | };""" 233 | 234 | [py:document] 235 | M2: """ 236 | class TextDocumentContentChangeEvent_0(TypedDict): 237 | 238 | # The range of the document that changed. 239 | range: Range 240 | 241 | # The optional length of the range that got replaced. 242 | 243 | # @deprecated use range instead. 244 | rangeLength: NotRequired[int] 245 | 246 | # The new text for the provided range. 247 | text: str 248 | class TextDocumentContentChangeEvent_1(TypedDict): 249 | 250 | # The new text of the whole document. 251 | text: str 252 | type TextDocumentContentChangeEvent = TextDocumentContentChangeEvent_0 | TextDocumentContentChangeEvent_1""" -------------------------------------------------------------------------------- /ts2python/singledispatch_shim.py: -------------------------------------------------------------------------------- 1 | """singledispatch_shim.py - singledispatch with forward-referenced types 2 | 3 | Python's functools.singledispatch does not work with function or 4 | method-signatures where the type of the dispatch-parameter is a 5 | forward-reference, i.e. where the dispatch-parameter is not annotated with 6 | a type itself, but with the string-name of the type. While the module "typing" 7 | allows this kind of annotation as a simple form of forward-referencing, 8 | "functools.singledispatch" raises a TypeError because of an unresolved 9 | reference. 10 | 11 | singledispatch_shim contains an alternative implementation of 12 | single dispatch that works correctly with forward-referenced types. 13 | 14 | Copyright 2022 by Eckhart Arnold (arnold@badw.de) 15 | Bavarian Academy of Sciences an Humanities (badw.de) 16 | 17 | Licensed under the Apache License, Version 2.0 (the "License"); 18 | you may not use this file except in compliance with the License. 19 | You may obtain a copy of the License at 20 | 21 | http://www.apache.org/licenses/LICENSE-2.0 22 | 23 | Unless required by applicable law or agreed to in writing, software 24 | distributed under the License is distributed on an "AS IS" BASIS, 25 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 26 | implied. See the License for the specific language governing 27 | permissions and limitations under the License. 28 | """ 29 | 30 | 31 | from functools import _find_impl, get_cache_token, update_wrapper 32 | from typing import Union, ForwardRef 33 | import sys 34 | try: 35 | from typing import get_args, get_origin, get_type_hints 36 | except ImportError: 37 | try: 38 | from typing_extensions import get_args, get_origin, get_type_hints 39 | except ImportError: 40 | from .typing_extensions import get_args, get_origin, get_type_hints 41 | 42 | 43 | try: 44 | import annotationlib 45 | def get_annotations(cls): 46 | try: 47 | return annotationlib.get_annotations(cls) 48 | except NameError: 49 | return True 50 | except (ImportError, ModuleNotFoundError): 51 | def get_annotations(cls): 52 | return getattr(cls, '__annotations__', {}) 53 | 54 | 55 | # The following functions have been copied from the Python 56 | # standard libraries typing-module. They have been adapted 57 | # to allow overloading functions that are annotated with 58 | # forward-referenced-types. 59 | 60 | 61 | def singledispatch(func): 62 | """Single-dispatch generic function decorator. 63 | 64 | Transforms a function into a generic function, which can have different 65 | behaviours depending upon the type of its first argument. The decorated 66 | function acts as the default implementation, and additional 67 | implementations can be registered using the register() attribute of the 68 | generic function. 69 | """ 70 | # There are many programs that use functools without singledispatch, so we 71 | # trade-off making singledispatch marginally slower for the benefit of 72 | # making start-up of such applications slightly faster. 73 | import types, weakref 74 | 75 | registry = {} 76 | dispatch_cache = weakref.WeakKeyDictionary() 77 | cache_token = None 78 | 79 | def dispatch(cls): 80 | """generic_func.dispatch(cls) -> 81 | 82 | Runs the dispatch algorithm to return the best available implementation 83 | for the given *cls* registered on *generic_func*. 84 | 85 | """ 86 | nonlocal cache_token 87 | if cache_token is not None: 88 | current_token = get_cache_token() 89 | if cache_token != current_token: 90 | dispatch_cache.clear() 91 | cache_token = current_token 92 | try: 93 | impl = dispatch_cache[cls] 94 | except KeyError: 95 | try: 96 | impl = registry[cls] 97 | except KeyError: 98 | if 'postponed' in registry: 99 | postponed = registry['postponed'] 100 | del registry['postponed'] 101 | for postponed_cls in postponed: 102 | register(postponed_cls) 103 | if 'postponed' in registry: 104 | for f in registry['postponed']: 105 | get_type_hints(f) # provoke NameError 106 | raise AssertionError('singledispatch: Internal Error: ' 107 | + str(registry['postponed'])) 108 | try: 109 | impl = registry[cls] 110 | except KeyError: 111 | impl = _find_impl(cls, registry) 112 | else: 113 | impl = _find_impl(cls, registry) 114 | dispatch_cache[cls] = impl 115 | return impl 116 | 117 | def _is_union_type(cls): 118 | try: 119 | return get_origin(cls) in {Union, types.UnionType} 120 | except AttributeError: 121 | # Python 3.7 122 | return get_origin(cls) in {Union} 123 | 124 | def _is_valid_dispatch_type(cls): 125 | if isinstance(cls, type): 126 | return True 127 | return (_is_union_type(cls) and 128 | all(isinstance(arg, type) for arg in get_args(cls))) 129 | 130 | def register(cls, func=None): 131 | """generic_func.register(cls, func) -> func 132 | 133 | Registers a new implementation for the given *cls* on a *generic_func*. 134 | 135 | """ 136 | nonlocal cache_token 137 | if _is_valid_dispatch_type(cls): 138 | if func is None: 139 | return lambda f: register(cls, f) 140 | else: 141 | if func is not None: 142 | raise TypeError( 143 | f"Invalid first argument to `register()`. " 144 | f"{cls!r} is not a class or union type." 145 | ) 146 | ann = get_annotations(cls) 147 | if not ann: 148 | if isinstance(cls, ForwardRef): 149 | obj = next(iter(registry.values())) 150 | mod = obj.__module__ 151 | method = cls.__forward_value__ 152 | cls.__forward_value__ = None 153 | if sys.version_info >= (3, 9, 0): 154 | cls = cls._evaluate(globals(), sys.modules[mod].__dict__, 155 | recursive_guard=set()) 156 | else: 157 | cls = cls._evaluate(globals(), sys.modules[mod].__dict__) 158 | else: 159 | raise TypeError( 160 | f"Invalid first argument to `register()`: {cls!r}. " 161 | f"Use either `@register(some_class)` or plain `@register` " 162 | f"on an annotated function." 163 | ) 164 | else: 165 | method = None 166 | func = cls 167 | 168 | # only import typing if annotation parsing is necessary 169 | try: 170 | argname, cls = next(iter(get_type_hints(func).items())) 171 | if not isinstance(cls, type) and str(type(cls))[1:6] == "class": 172 | raise NameError 173 | except NameError: 174 | if cls != func: 175 | try: 176 | cls.__forward_value__ = func 177 | except AttributeError as e: 178 | pass # TODO: Is this risky? 179 | # raise(e) 180 | registry.setdefault('postponed', []).append(cls) 181 | return func 182 | except StopIteration: 183 | func = method 184 | if not _is_valid_dispatch_type(cls): 185 | if _is_union_type(cls): 186 | raise TypeError( 187 | f"Invalid annotation for {argname!r}. " 188 | f"{cls!r} not all arguments are classes." 189 | ) 190 | else: 191 | raise TypeError( 192 | f"Invalid annotation for {argname!r}. " 193 | f"{cls!r} of type {type(cls)!r} is not a class." 194 | ) 195 | 196 | if _is_union_type(cls): 197 | for arg in get_args(cls): 198 | registry[arg] = func 199 | else: 200 | registry[cls] = func 201 | if cache_token is None and hasattr(cls, '__abstractmethods__'): 202 | cache_token = get_cache_token() 203 | dispatch_cache.clear() 204 | return func 205 | 206 | def wrapper(*args, **kw): 207 | if not args: 208 | raise TypeError(f'{funcname} requires at least ' 209 | '1 positional argument') 210 | 211 | return dispatch(args[0].__class__)(*args, **kw) 212 | 213 | funcname = getattr(func, '__name__', 'singledispatch function') 214 | registry[object] = func 215 | wrapper.register = register 216 | wrapper.dispatch = dispatch 217 | wrapper.registry = types.MappingProxyType(registry) 218 | wrapper._clear_cache = dispatch_cache.clear 219 | update_wrapper(wrapper, func) 220 | return wrapper 221 | 222 | 223 | class singledispatchmethod: 224 | """Single-dispatch generic method descriptor. 225 | 226 | Supports wrapping existing descriptors and handles non-descriptor 227 | callables as instance methods. 228 | """ 229 | 230 | def __init__(self, func): 231 | if not callable(func) and not hasattr(func, "__get__"): 232 | raise TypeError(f"{func!r} is not callable or a descriptor") 233 | 234 | self.dispatcher = singledispatch(func) 235 | self.func = func 236 | 237 | def register(self, cls, method=None): 238 | """generic_method.register(cls, func) -> func 239 | 240 | Registers a new implementation for the given *cls* on a *generic_method*. 241 | """ 242 | return self.dispatcher.register(cls, func=method) 243 | 244 | def __get__(self, obj, cls=None): 245 | def _method(*args, **kwargs): 246 | method = self.dispatcher.dispatch(args[0].__class__) 247 | return method.__get__(obj, cls)(*args, **kwargs) 248 | 249 | _method.__isabstractmethod__ = self.__isabstractmethod__ 250 | _method.register = self.register 251 | update_wrapper(_method, self.func) 252 | return _method 253 | 254 | @property 255 | def __isabstractmethod__(self): 256 | return getattr(self.func, '__isabstractmethod__', False) 257 | 258 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. ts2python documentation master file, created by 2 | sphinx-quickstart on Fri Oct 8 08:32:17 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. default-domain::py 7 | 8 | ts2python Documentation 9 | ======================= 10 | 11 | ts2python_ is a transpiler that converts `TypeScript-interfaces`_ to Python_ 12 | TypedDict_-classes and provides runtime json_-validation against those 13 | interfaces/TypedDicts. 14 | 15 | ts2python_ is licensed under the `Apache-2.0`_ open source license. 16 | The source code can be cloned freely from: 17 | `https://github.com/jecki/ts2python `_ 18 | 19 | Installation 20 | ------------ 21 | 22 | ts2python_ can be installed as usual with Python's package-manager "pip":: 23 | 24 | $ pip install ts2python 25 | 26 | The ts2python-transpiler requires at least Python_ Version 3.8 to run. 27 | However, the output that ts2python produces is backwards compatible 28 | with Python 3.7, unless otherwise specified (see below). The only 29 | dependency of ts2python is the parser-generator DHParser_. 30 | 31 | Usage 32 | ----- 33 | 34 | The easiest way to use ts2python is by running **ts2pythonExplorer**, 35 | which provides a simple graphical user interface to ts2python. 36 | You can just paste your Typescript-Interface definitions into 37 | the top text-area and click on the "Generate Python-Code"-button. 38 | You can then simply copy the generated code from the text-area 39 | below into you Python source code. 40 | 41 | ts2pythonExplorer also allows you to experiment with different 42 | version-compatibility settings for the generated Python-Code. 43 | These settings can be saved as a configuration file to your 44 | project's home-directory from where the ts2pyhon-command will 45 | automatically apply them without the need of specifying any 46 | particular command line options every time it is called. 47 | 48 | From the command line, generating Python-pendants for Typescript-interfaces 49 | is as simple as calling:: 50 | 51 | $ ts2python interfaces.ts 52 | 53 | and then importing the generated ``interfaces.py`` by:: 54 | 55 | from interfaces import * 56 | 57 | For every typescript interface in the ``interfaces.ts`` file the generated 58 | Python module contains a TypedDict-class with the same name that defines 59 | the same data structure as the Typescript-interface. Typescript-data serialized 60 | as json can simply be deserialized by Python-code as long as you know the 61 | type of the root data structure beforehand. Say, the root data structure 62 | is ``RequestMessage`` than retrieving the RequestMessage-object from a 63 | json-string generated by a Typescript-script requires nothing more than 64 | deserializing this string:: 65 | 66 | import json 67 | request_msg: RequestMessage = json.loads(input_data) 68 | 69 | The only requirement is that the root-type of the json-data is known beforehand. 70 | Everything else simply falls into place. 71 | 72 | Backwards compatibility 73 | ----------------------- 74 | 75 | ts2python tries to be as backwards compatible as possible. To run ts2python you 76 | need at least Python version 3.8. The code ts2python generates is backwards 77 | compatible down to version 3.7. If you do not need to be compatible with older 78 | versions, you can use the --compatibility [VERSION] switch to generate code 79 | for newer versions only, e.g.:: 80 | 81 | $ ts2python --compatibility 3.11 [FILENAME.ts] 82 | 83 | Usually, this code is somewhat cleaner than the fully 84 | compatible code. Also, certain features like ``type``-statments (Python 3.12 and 85 | above) or the ``ReadOnly``-qualifier (Python 3.13 and higher) are only available 86 | at higher compatibility levels! 87 | In order to achieve full conformity with most type-checkers, it is advisable 88 | to use compatibility level 3.11 and also add the ``-a toplevel`` switch 89 | to always turn anonymous TypeScript-interfaces into top-level classes, rather 90 | than locally defined classes. Local classes are not allowed for Python TypedDicts, 91 | although they work perfectly well - except that type-checkers like pylance 92 | emit an error-message. 93 | 94 | With compatibility-level 3.11 and above, the generated code does not need to 95 | use ts2python's "typeddict_shim"-compatibility layer, any more. This greatly 96 | simplifies the import-block at the beginning of the generated code and 97 | eliminates one of two dependencies of the generated code on the ts2python-package. 98 | 99 | The other remaining dependency "singledispatch_shim" can be removed from the 100 | generated code by hand, if there are no single-dispatch functions in the 101 | code that dispatch on a forward-referenced type, i.e. a type that is denoted 102 | in the code by a string containing the type-name rather than the type-name 103 | directly. (It's a current limitation of functools.singledispatch that it 104 | cannot handle forward references.) 105 | 106 | Command-line switches 107 | --------------------- 108 | 109 | The Python output of ts2pythonParser can be controlled by the following 110 | command line-switches: 111 | 112 | * ``-c`` followed by a Python Version-number, e.g. ``-c 3.12`` 113 | 114 | * ``-p`` followed by a comma separated list of `PEP`_-numbers, e.g. 115 | ``-p 563,601``. Supported PEPs are: 116 | 117 | * `435`_ - use Enums (Python 3.4) 118 | * `563`_ - use postponed evaluation (Python 3.7) 119 | * `584`_ or `586`_ - use Literal type (Python 3.8) 120 | * `604`_ - use type union (Python 3.10) 121 | * `613`_ - use explicit type alias (Python 3.10 - 3.11) 122 | * `646`_ - use variadic Generics (Python 3.11) 123 | * `649`_ or `749`_ - assume deferred type evaluation (Python 3.14) 124 | * `655`_ - use NotRequired instead of Optional (Python3.11) 125 | * `695`_ - use type parameters (Python 3.12) 126 | * `705`_ - allow ReadOnly (Python 3.13) 127 | 128 | Setting a Python Version-Number with the ``-c`` switch also 129 | automatically sets all PEPs 130 | that have been implemented with that version, except for PEP `563`_ which 131 | must be set explicitly with the ``-p 563`` switch as this concerns an 132 | optional feature for Python-Versions 3.7-3.12 which will be turned on 133 | with the ``from future import __annotation__`` statement at the beginnig 134 | of the generated source code. 135 | 136 | * ``-k`` preserves Typescript-multiline comments and adds them as 137 | Python-comments to the generated source-code 138 | 139 | * ``-a`` followed by one of the four possible keywords ``local`` (default), 140 | ``toplevel``, ``functional``, ``type``. These are four different styles 141 | for transpiling anonymous interfaces. The default rule ``local`` is not 142 | strictly conformant with the type-rules for TypedDicts. For full type-checker 143 | conformance use ``toplevel``. The other two keywords, "functional" and "type" 144 | should be considered as experimental as they have seen little testing. 145 | 146 | * ``-o`` followed by the name of an output-directory for the generated Python 147 | code. If an output-directory is chosen the results will be written as files 148 | to this directory, rather than printed to the console (stdout). 149 | 150 | 151 | Current Limitations 152 | ------------------- 153 | 154 | Presently, ts2python is mostly limited to Typescript-Interfaces that do not 155 | contain any methods. The language server-protocol-definitions can be transpiled 156 | successfully. 157 | 158 | Hacking ts2python 159 | ----------------- 160 | 161 | Hacking ts2python is not easy. (Sorry!) The reason is that ts2python 162 | was primarily designed for a relatively limited application case, i.e. 163 | transpiling interface definitions. In order to keep things simple, the 164 | abstract syntax tree (AST) from the TypeScript source is directly converted 165 | to Python code, rather than transforming the TypeScript-AST to a Python-AST 166 | first. 167 | 168 | Adding such a tree-to-tree-transformation before the Python code 169 | generation stage certainly would have made sense - 170 | among other things, because some components need to be re-ordered, 171 | since Python does not know anonymous classes/interfaces. 172 | However, for the above mentioned restricted purpose this appeared to me like 173 | overengineering. Meanwhile, I have come to regret that, 174 | because it makes adding more features harder. 175 | 176 | In order to keep track of code snippets 177 | that must be reordered or of names and scopes that need to be completed 178 | or filled in later or, more generally, for any task that cannot be 179 | completed locally, when transforming a particular node of the AST, 180 | the present implementation keeps several different stacks in 181 | instance-variables of the ts2pythonCompiler-object, namely, 182 | ``known_types``, ``local_classes``, ``base_classes``, ``obj_name``, 183 | ``scope_type``, ``optional_keys``. Lookout for where those stacks are used 184 | when you to change something in the ts2python-source yourself! 185 | 186 | Alternatively, you can also use ts2python to just output the AST of 187 | a Typescript-file with interface definitions by using the ``--target AST`` 188 | command line swith and then start from there. Example:: 189 | 190 | $ ts2python --target AST --serialize XML interfaces.ts 191 | 192 | Other serialiazations for the AST are available: SXML (S-expression), 193 | ndst, json or simply an indented tree. 194 | 195 | .. _ts2python: https://github.com/jecki/ts2python/ 196 | .. _Typescript-interfaces: https://www.typescriptlang.org/docs/handbook/2/objects.html 197 | .. _TypedDict: https://www.python.org/dev/peps/pep-0589/ 198 | .. _json: https://www.json.org/ 199 | .. _Apache-2.0: https://www.apache.org/licenses/LICENSE-2.0 200 | .. _Python: https://www.python.org/ 201 | .. _DHParser: https://gitlab.lrz.de/badw-it/DHParser 202 | .. _pydantic: https://pydantic-docs.helpmanual.io/ 203 | .. _attr: https://www.attrs.org/ 204 | .. _ts2PythonParser.py: https://github.com/jecki/ts2python/blob/main/ts2pythonParser.py 205 | .. _json_validation: https://github.com/jecki/ts2python/blob/main/ts2python/json_validation.py 206 | .. _typing_extensions: https://github.com/python/typing/blob/master/typing_extensions/README.rst 207 | .. _PEP: https://peps.python.org/ 208 | .. _435: https://peps.python.org/pep-0435/ 209 | .. _563: https://peps.python.org/pep-0563/ 210 | .. _584: https://peps.python.org/pep-0584/ 211 | .. _586: https://peps.python.org/pep-0586/ 212 | .. _604: https://peps.python.org/pep-0604/ 213 | .. _613: https://peps.python.org/pep-0613/ 214 | .. _646: https://peps.python.org/pep-0646/ 215 | .. _649: https://peps.python.org/pep-0649/ 216 | .. _655: https://peps.python.org/pep-0655/ 217 | .. _695: https://peps.python.org/pep-0695/ 218 | .. _705: https://peps.python.org/pep-0705/ 219 | .. _749: https://peps.python.org/pep-0749/ 220 | 221 | .. toctree:: 222 | :maxdepth: 2 223 | :caption: Contents: 224 | 225 | BasicUsage.rst 226 | Mapping.rst 227 | Validation.rst 228 | 229 | Indices and tables 230 | ================== 231 | 232 | * :ref:`genindex` 233 | * :ref:`modindex` 234 | * :ref:`search` 235 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts2python 2 | 3 | ![](https://img.shields.io/pypi/v/ts2python) 4 | ![](https://img.shields.io/pypi/status/ts2python) 5 | ![](https://img.shields.io/pypi/pyversions/ts2python) 6 | ![](https://img.shields.io/pypi/l/ts2python) 7 | [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua) 8 | 9 | 10 | Python-interoperability for Typescript-Interfaces. 11 | Transpiles TypeScript-Interface-definitions to Python 12 | TypedDicts, plus support for run-time type-checking 13 | of JSON-data. 14 | 15 | ## License and Source Code 16 | 17 | ts2python is open source software under the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0) 18 | 19 | Copyright 2021-2023 Eckhart Arnold , Bavarian Academy of Sciences and Humanities 20 | 21 | Licensed under the Apache License, Version 2.0 (the "License"); 22 | you may not use this file except in compliance with the License. 23 | You may obtain a copy of the License at 24 | 25 | https://www.apache.org/licenses/LICENSE-2.0 26 | 27 | Unless required by applicable law or agreed to in writing, software 28 | distributed under the License is distributed on an "AS IS" BASIS, 29 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 30 | See the License for the specific language governing permissions and 31 | limitations under the License. 32 | 33 | The complete source-code of ts2python can be downloaded from the 34 | [git-repository](https://github.com/jecki/ts2python). 35 | 36 | ## Purpose 37 | 38 | When processing JSON data, as for example, form a 39 | [JSON-RPC](https://www.jsonrpc.org/) call, with Python, it would 40 | be helpful to have Python-definitions of the JSON-structures at 41 | hand, in order to solicit IDE-Support, static type checking and, 42 | potentially to enable structural validation at runtime. 43 | 44 | There exist different technologies for defining the structure of 45 | JSON-data. Next to [JSON-schema](http://json-schema.org/), a 46 | de facto very popular technology for defining JSON-obejcts are 47 | [Typescript-Interfaces](https://www.typescriptlang.org/docs/handbook/2/objects.html). 48 | For example, the 49 | [language server protocol](https://microsoft.github.io/language-server-protocol/specifications/specification-current/) 50 | defines the structure of the JSON-data exchanged between client 51 | and server with Typescript-Interfaces. 52 | 53 | In order to enable structural validation on the Python-side, 54 | ts2python transpiles the typescript-interface definitions 55 | to Python-data structure definitions, primarily, 56 | [TypedDicts](https://www.python.org/dev/peps/pep-0589/), 57 | but with some postprocessing it can also be adjusted to 58 | other popular models for records or data structures in 59 | Python, e.g. 60 | [pydantic](https://pydantic-docs.helpmanual.io/)-Classes 61 | and the like. 62 | 63 | ts2python aims to support translation of TypeScript-Interfaces on two 64 | different tiers: 65 | 66 | 1. *Tier 1: Transpilation of passive data-structures*, that is, 67 | Typescript-definition-files that contain only data definitions 68 | and no function definitions and, in particular, 69 | only "passive" Typescript-Interface that define data-structures 70 | but do not contain any methods. 71 | 72 | 2. *Tier 2: Tanspilation of active data-structures, function- 73 | and method-definitions*, i.e. Translation of (almost) any 74 | Typescript-definition-file. 75 | 76 | ## Status 77 | 78 | Presently, Tier 1 support, i.e. transpilation of passive data 79 | structures works quite well. So, for example, all Interfaces 80 | from the 81 | [language server protocol V3.17](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/) 82 | can be transpiled to Python Typed-Dicts. 83 | 84 | Tier 2 support is and will remain work in progress. I am 85 | using "vscode.d.ts"-definition file as test-case. Most things 86 | ins "vscode.d.ts" work, but there are still some unsupported 87 | constructs. 88 | 89 | The documentation presently only covers Tier 1 support. 90 | 91 | 92 | ## Installation 93 | 94 | ts2python can be installed from the command line with the command: 95 | 96 | $ pip install ts2python 97 | 98 | ts2python requires the parsing-expression-grammar-framework 99 | [DHParser](https://gitlab.lrz.de/badw-it/DHParser) 100 | which will automatically be installed as a dependency by 101 | the `pip`-command. ts2python requires at least Python Version 3.8 102 | to run. (If there is any interest, I might backport it to Python 3.6.) 103 | However, the Python-code it produces is backwards compatible 104 | down to Python 3.6, if the 105 | [typing extensions](https://pypi.org/project/typing-extensions/) 106 | have been installed. 107 | 108 | ## Usage 109 | 110 | ### ts2python GUI 111 | 112 | The easiest way to use ts2python is by running ts2pythonExplorer, 113 | which provides a simple graphical user interface to ts2python. 114 | You can just paste your Typescript-Interface definitions into 115 | the top text-area and click on the "Generate Python-Code"-button. 116 | 117 | ![Screenshot of ts2pythonExplorer](screenshot.png) 118 | 119 | ### ts2python Command Line 120 | 121 | In order to generate TypedDict-classes from Typescript-Interfaces, 122 | run `ts2python` on the Typescript-Interface definitions: 123 | 124 | $ ts2python interfaces.ts 125 | 126 | This generates a .py-file in same directory as the source 127 | file that contains the TypedDict-classes and can simpy be 128 | imported in Python-Code: 129 | 130 | from interfaces import * 131 | 132 | JSON-data which adheres to a specific structure (no matter 133 | whether defined on the typescript side via interfaces or 134 | on the Python-side via TypedDicts) can easily be interchanged 135 | and deserialized: 136 | 137 | import json 138 | request_msg: RequestMessage = json.loads(input_data) 139 | 140 | The root-type (``RootMessage`` in the above example) can 141 | be arbitrarily complex and deeply nested. 142 | 143 | **Calling ts2python from another Python-script**: 144 | 145 | The ts2python-Parser can, of course, also be accessed directly 146 | from Python with the followin imports and function calls: 147 | 148 | from ts2python.ts2pthonParser import process_file 149 | ... 150 | process_file("SOURCE.ts", "DESTINATION.py") 151 | 152 | Or, use: 153 | 154 | from ts2pthon.ts2pythonParser import compile_src, serialize_result 155 | ... 156 | result, errors = compile_src(DOCUMENT) 157 | if errors: 158 | for e in errors: print(e) 159 | else: 160 | print(serialize_result(result)) 161 | 162 | 163 | ## Validation 164 | 165 | ts2python ships support for runtime-type validation. While type 166 | errors can be detected by static type checkers, runtime type 167 | validation can be useful when processing data from an outside 168 | source which cannot statically be checked, like, for example, 169 | json-data stemming from an RPC-call. ts2python runtime-type 170 | validation can be invoked via dedicated functions or via 171 | decorator as in this example: 172 | 173 | from ts2python.json_validation import TypedDict, type_check 174 | 175 | class Position(TypedDict, total=True): 176 | line: int 177 | character: int 178 | 179 | class Range(TypedDict, total=True): 180 | start: Position 181 | end: Position 182 | 183 | @type_check 184 | def middle_line(rng: Range) -> Position: 185 | line = (rng['start']['line'] + rng['end']['line']) // 2 186 | character = 0 187 | return Position(line=line, character=character) 188 | 189 | data = {'start': {'line': 1, 'character': 1}, 190 | 'end': {'line': 8, 'character': 17}} 191 | assert middle_line(data) == {'line': 4, 'character': 0} 192 | 193 | malformed_data = {'start': 1, 'end': 8} 194 | middle_line(malformed_data) # <- TypeError raised by @type_check 195 | 196 | With the type decorator the last call fails with a TypeError: 197 | 198 | TypeError: Parameter "rng" of function "middle_line" failed the type-check, because: 199 | Type error(s) in dictionary of type : 200 | Field start: '1' is not of , but of type 201 | Field end: '8' is not of , but of type 202 | 203 | Both the call and the return types can be validated. 204 | 205 | 206 | ## Python-Version compatibility 207 | 208 | By default, ts2python generates code that is compatibel with Python 209 | 3.7 and above. This code is not strictly standard-conform, because 210 | it imitates certain features that became available only with higher 211 | versions (like "NotRequired" for individual TypedDict-Fields) with 212 | other means. "Not strictly standard-conform" means, the code works, 213 | but type-checkers might complain. 214 | 215 | In order to generate code for higher Python-Versions only, you 216 | can use the compatibility switch from the command line: 217 | 218 | $ ts2python --compatibility 3.11 interfaces.ts 219 | 220 | In this example, the generated code is compatible only with 221 | Python version 3.11 and above. To achive full compatibility 222 | with type checkers (e.g. Pylance) it is advisable also use the 223 | `-a toplevel` switch (see below). 224 | 225 | 226 | ## Type Checkers 227 | 228 | The output ts2python is somewhat more liberal than what strict 229 | typecheckers like mypy or pylance seem to allow. In particular 230 | class definitions inside TypedDicts are considered illegal by 231 | the specification und thus marked as an error by some 232 | type-checkers. Use the command-line switch `-a toplevel` to 233 | generate Python-code that is more acceptable to type checkers, e.g.: 234 | 235 | $ ts2python --compatibility 3.11 -a toplevel interfaces.ts 236 | 237 | However, IMHO defining nested anonymous TypeScript interfaces on the 238 | toplevel in the Python code can make the code quite a bit less 239 | readable thatn allowing ts2python to define them as local classes 240 | of TypedDict-classes. 241 | 242 | ## Full Documentation 243 | 244 | See [ts2python.readthedocs.io](https://ts2python.readthedocs.io) for the comprehensive 245 | documentation of ts2python 246 | 247 | 248 | ## Tests and Demonstration 249 | 250 | The [git-repository of ts2python](https://github.com/jecki/ts2python) contains unit-tests 251 | as well as [doctests](https://docs.python.org/3/library/doctest.html). 252 | After cloning ts2python from the git-repository with: 253 | 254 | $ git clone https://github.com/jecki/ts2python 255 | 256 | the unit tests can be found in the `tests` subdirectory. 257 | Both the unit and the doctests can be run by changing to the 258 | `tests`-sub-directory and calling the `runner.py`-skript therein. 259 | 260 | $ cd tests 261 | $ python runner.py 262 | 263 | It is also possible to run the tests with [pytest](https://docs.pytest.org/) 264 | or [nose](https://github.com/nose-devs/nose2), in case you have 265 | either of theses testing-frameworks installed. 266 | 267 | For a demonstration how the TypeScript-Interfaces are transpiled 268 | to Python-code, run the `demo.sh`-script (or `demo.bat` on Windows) 269 | in the "demo"-sub-directory or the ts2python-repository. 270 | 271 | Once, you have run the `demo.sh`-script you can also run the 272 | `test.sh` which tests the compatibility with every Python-Version 273 | higher or equal than 3.7 that is installed on your system. 274 | 275 | Or, run the `tst_ts2python_grammar.py` in the ts2python-directory 276 | and look up the grammar-test-reports in the "REPORT"-sub-directory 277 | of the "test_grammar"-subdirectory. 278 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /tests_grammar/12_test_anonymous_toplevel.ini: -------------------------------------------------------------------------------- 1 | [config] 2 | ts2python.RenderAnonymous = "toplevel" 3 | ts2python.UseTypeUnion = "True" 4 | ts2python.UseNotRequired = True 5 | ts2python.UseLiteralType = True 6 | ts2python.UseTypeParameters = True 7 | ts2python.UseVariadicGenerics = True 8 | ts2python.UsePostponedEvaluation = False 9 | ts2python.AllowReadOnly = False 10 | 11 | 12 | [match:interface] 13 | M1: """interface InitializeResult { 14 | capabilities: ServerCapabilities; 15 | serverInfo?: { 16 | name: string; 17 | version?: string; 18 | }; 19 | }""" 20 | M2: """interface SemanticTokensClientCapabilities { 21 | dynamicRegistration?: boolean; 22 | requests: { 23 | range?: boolean | { 24 | }; 25 | full?: boolean | { 26 | delta?: boolean; 27 | }; 28 | }; 29 | tokenTypes: string[]; 30 | tokenModifiers: string[]; 31 | formats: TokenFormat[]; 32 | overlappingTokenSupport?: boolean; 33 | multilineTokenSupport?: boolean; 34 | }""" 35 | M3: """export class Position { 36 | readonly line: number; 37 | readonly character: number; 38 | constructor(line: number, character: number); 39 | isBefore(other: Position): boolean; 40 | isBeforeOrEqual(other: Position): boolean; 41 | isAfter(other: Position): boolean; 42 | isAfterOrEqual(other: Position): boolean; 43 | isEqual(other: Position): boolean; 44 | compareTo(other: Position): number; 45 | translate(lineDelta?: number, characterDelta?: number): Position; 46 | translate(change: { lineDelta?: number; characterDelta?: number }): Position; 47 | with(line?: number, character?: number): Position; 48 | with(change: { line?: number; character?: number }): Position; 49 | }""" 50 | M4: """export interface NotebookDocumentSyncOptions { 51 | notebookSelector: ({ 52 | notebook: string | NotebookDocumentFilter; 53 | cells?: { language: string }[]; 54 | } | { 55 | notebook?: string | NotebookDocumentFilter; 56 | cells: { language: string }[]; 57 | })[]; 58 | save?: boolean; 59 | }""" 60 | M5: """interface Test { 61 | export function registerWebviewViewProvider(viewId: string, provider: WebviewViewProvider, options?: { 62 | readonly webviewOptions?: { 63 | readonly retainContextWhenHidden?: boolean; 64 | }; 65 | }): Disposable; 66 | }""" 67 | M6: """interface CalendarJSON { 68 | date: string; 69 | fiscal: { 70 | month: MonthNumbers; 71 | quarter: 72 | | { name: 'Q1'; value: 1 } 73 | | { name: 'Q2'; value: 2 } 74 | | { name: 'Q3'; value: 3 } 75 | | { name: 'Q4'; value: 4 }; 76 | week: WeekNumbers; 77 | year: CommonYears; 78 | }; 79 | gregorian: { 80 | day_of_week: 81 | | { name: 'Monday'; value: 0 } 82 | | { name: 'Tuesday'; value: 1 } 83 | | { name: 'Wednesday'; value: 2 } 84 | | { name: 'Thursday'; value: 3 } 85 | | { name: 'Friday'; value: 4 } 86 | | { name: 'Saturday'; value: 5 } 87 | | { name: 'Sunday'; value: 6 }; 88 | month: MonthNumbers; 89 | quarter: QuarterNumbers; 90 | week: WeekNumbers; 91 | year: CommonYears; 92 | }; 93 | id: string; 94 | }""" 95 | 96 | [py:interface] 97 | M1: """class InitializeResult_ServerInfo_0(TypedDict): 98 | name: str 99 | version: NotRequired[str] 100 | 101 | class InitializeResult(TypedDict): 102 | capabilities: 'ServerCapabilities' 103 | serverInfo: NotRequired[InitializeResult_ServerInfo_0]""" 104 | 105 | M2: """class SemanticTokensClientCapabilities_Requests_0_Range_1(TypedDict): 106 | pass 107 | class SemanticTokensClientCapabilities_Requests_0_Full_1(TypedDict): 108 | delta: NotRequired[bool] 109 | class SemanticTokensClientCapabilities_Requests_0(TypedDict): 110 | range: NotRequired[bool | SemanticTokensClientCapabilities_Requests_0_Range_1] 111 | full: NotRequired[bool | SemanticTokensClientCapabilities_Requests_0_Full_1] 112 | 113 | class SemanticTokensClientCapabilities(TypedDict): 114 | dynamicRegistration: NotRequired[bool] 115 | requests: SemanticTokensClientCapabilities_Requests_0 116 | tokenTypes: List[str] 117 | tokenModifiers: List[str] 118 | formats: List['TokenFormat'] 119 | overlappingTokenSupport: NotRequired[bool] 120 | multilineTokenSupport: NotRequired[bool]""" 121 | 122 | M3: """class Position: 123 | class TranslateChange_0(TypedDict): 124 | lineDelta: NotRequired[float] 125 | characterDelta: NotRequired[float] 126 | class With_Change_0(TypedDict): 127 | line: NotRequired[float] 128 | character: NotRequired[float] 129 | line: float 130 | character: float 131 | 132 | def isBefore(self, other: 'Position') -> bool: 133 | pass 134 | 135 | def isBeforeOrEqual(self, other: 'Position') -> bool: 136 | pass 137 | 138 | def isAfter(self, other: 'Position') -> bool: 139 | pass 140 | 141 | def isAfterOrEqual(self, other: 'Position') -> bool: 142 | pass 143 | 144 | def isEqual(self, other: 'Position') -> bool: 145 | pass 146 | 147 | def compareTo(self, other: 'Position') -> float: 148 | pass 149 | 150 | @singledispatchmethod 151 | def translate(self, arg1) -> 'Position': 152 | raise TypeError(f'First argument {arg1} of single-dispatch function/method {name} has illegal type {type(arg1)}') 153 | 154 | @translate.register 155 | def _(self, lineDelta: Optional[float] = None, characterDelta: Optional[float] = None) -> 'Position': 156 | pass 157 | 158 | @translate.register 159 | def _(self, change: TranslateChange_0) -> 'Position': 160 | pass 161 | 162 | @singledispatchmethod 163 | def with_(self, arg1) -> 'Position': 164 | raise TypeError(f'First argument {arg1} of single-dispatch function/method {name} has illegal type {type(arg1)}') 165 | 166 | @with_.register 167 | def _(self, line: Optional[float] = None, character: Optional[float] = None) -> 'Position': 168 | pass 169 | 170 | @with_.register 171 | def _(self, change: With_Change_0) -> 'Position': 172 | pass""" 173 | 174 | M4: """class NotebookDocumentSyncOptions_NotebookSelector_0_Cells_0(TypedDict): 175 | language: str 176 | class NotebookDocumentSyncOptions_NotebookSelector_0(TypedDict): 177 | notebook: 'str | NotebookDocumentFilter' 178 | cells: NotRequired[List[NotebookDocumentSyncOptions_NotebookSelector_0_Cells_0]] 179 | class NotebookDocumentSyncOptions_NotebookSelector_1_Cells_0(TypedDict): 180 | language: str 181 | class NotebookDocumentSyncOptions_NotebookSelector_1(TypedDict): 182 | notebook: NotRequired['str | NotebookDocumentFilter'] 183 | cells: List[NotebookDocumentSyncOptions_NotebookSelector_1_Cells_0] 184 | 185 | class NotebookDocumentSyncOptions(TypedDict): 186 | notebookSelector: List[NotebookDocumentSyncOptions_NotebookSelector_0 | NotebookDocumentSyncOptions_NotebookSelector_1] 187 | save: NotRequired[bool]""" 188 | 189 | M5: """class Test: 190 | class RegisterWebviewViewProviderOptions_0(TypedDict): 191 | class WebviewOptions_0(TypedDict): 192 | retainContextWhenHidden: NotRequired[bool] 193 | webviewOptions: NotRequired[WebviewOptions_0] 194 | 195 | def registerWebviewViewProvider(self, viewId: str, provider: 'WebviewViewProvider', options: Optional[RegisterWebviewViewProviderOptions_0] = None) -> 'Disposable': 196 | pass""" 197 | 198 | M6: """class CalendarJSON_Fiscal_0_Quarter_0(TypedDict): 199 | name: Literal["Q1"] 200 | value: Literal[1] 201 | class CalendarJSON_Fiscal_0_Quarter_1(TypedDict): 202 | name: Literal["Q2"] 203 | value: Literal[2] 204 | class CalendarJSON_Fiscal_0_Quarter_2(TypedDict): 205 | name: Literal["Q3"] 206 | value: Literal[3] 207 | class CalendarJSON_Fiscal_0_Quarter_3(TypedDict): 208 | name: Literal["Q4"] 209 | value: Literal[4] 210 | class CalendarJSON_Fiscal_0(TypedDict): 211 | month: 'MonthNumbers' 212 | quarter: CalendarJSON_Fiscal_0_Quarter_0 | CalendarJSON_Fiscal_0_Quarter_1 | CalendarJSON_Fiscal_0_Quarter_2 | CalendarJSON_Fiscal_0_Quarter_3 213 | week: 'WeekNumbers' 214 | year: 'CommonYears' 215 | class CalendarJSON_Gregorian_0_Day_of_week_0(TypedDict): 216 | name: Literal["Monday"] 217 | value: Literal[0] 218 | class CalendarJSON_Gregorian_0_Day_of_week_1(TypedDict): 219 | name: Literal["Tuesday"] 220 | value: Literal[1] 221 | class CalendarJSON_Gregorian_0_Day_of_week_2(TypedDict): 222 | name: Literal["Wednesday"] 223 | value: Literal[2] 224 | class CalendarJSON_Gregorian_0_Day_of_week_3(TypedDict): 225 | name: Literal["Thursday"] 226 | value: Literal[3] 227 | class CalendarJSON_Gregorian_0_Day_of_week_4(TypedDict): 228 | name: Literal["Friday"] 229 | value: Literal[4] 230 | class CalendarJSON_Gregorian_0_Day_of_week_5(TypedDict): 231 | name: Literal["Saturday"] 232 | value: Literal[5] 233 | class CalendarJSON_Gregorian_0_Day_of_week_6(TypedDict): 234 | name: Literal["Sunday"] 235 | value: Literal[6] 236 | class CalendarJSON_Gregorian_0(TypedDict): 237 | day_of_week: CalendarJSON_Gregorian_0_Day_of_week_0 | CalendarJSON_Gregorian_0_Day_of_week_1 | CalendarJSON_Gregorian_0_Day_of_week_2 | CalendarJSON_Gregorian_0_Day_of_week_3 | CalendarJSON_Gregorian_0_Day_of_week_4 | CalendarJSON_Gregorian_0_Day_of_week_5 | CalendarJSON_Gregorian_0_Day_of_week_6 238 | month: 'MonthNumbers' 239 | quarter: 'QuarterNumbers' 240 | week: 'WeekNumbers' 241 | year: 'CommonYears' 242 | 243 | class CalendarJSON(TypedDict): 244 | date: str 245 | fiscal: CalendarJSON_Fiscal_0 246 | gregorian: CalendarJSON_Gregorian_0 247 | id: str""" 248 | 249 | 250 | [match:document] 251 | M1: """namespace Window { 252 | export function withProgress(options: ProgressOptions, task: (progress: Progress<{ 253 | message?: string; 254 | increment?: number; 255 | }>, token: CancellationToken) => Thenable): Thenable; 256 | }""" 257 | M2: """export namespace window { 258 | export function createOutputChannel(name: string, languageId?: string): OutputChannel; 259 | export function createOutputChannel(name: string, options: { log: true }): LogOutputChannel; 260 | export function createWebviewPanel(viewType: string, title: string, showOptions: ViewColumn | { 261 | readonly viewColumn: ViewColumn; 262 | readonly preserveFocus?: boolean; 263 | }, options?: WebviewPanelOptions & WebviewOptions): WebviewPanel; 264 | export function registerWebviewViewProvider(viewId: string, provider: WebviewViewProvider, options?: { 265 | readonly webviewOptions?: { 266 | readonly retainContextWhenHidden?: boolean; 267 | }; 268 | }): Disposable; 269 | export function registerCustomEditorProvider(viewType: string, provider: CustomTextEditorProvider | CustomReadonlyEditorProvider | CustomEditorProvider, options?: { 270 | readonly webviewOptions?: WebviewPanelOptions; 271 | readonly supportsMultipleEditorsPerDocument?: boolean; 272 | }): Disposable; 273 | export const onDidChangeActiveColorTheme: Event; 274 | }""" 275 | 276 | [py:document] 277 | M1: """ 278 | class Window_WithProgressTask_0_Progress_0[R](TypedDict): 279 | message: NotRequired[str] 280 | increment: NotRequired[float] 281 | 282 | class Window: 283 | def withProgress[R](options: 'ProgressOptions', task: Callable[['Progress[Window_WithProgressTask_0_Progress_0]', 'CancellationToken'], 'Thenable[R]']) -> 'Thenable[R]': 284 | pass""" 285 | M2: """ 286 | class window_CreateOutputChannelOptions_0(TypedDict): 287 | log: Literal[True] 288 | class window_CreateWebviewPanelShowOptions_1(TypedDict): 289 | viewColumn: 'ViewColumn' 290 | preserveFocus: NotRequired[bool] 291 | class window_RegisterWebviewViewProviderOptions_0_WebviewOptions_0(TypedDict): 292 | retainContextWhenHidden: NotRequired[bool] 293 | class window_RegisterWebviewViewProviderOptions_0(TypedDict): 294 | webviewOptions: NotRequired[window_RegisterWebviewViewProviderOptions_0_WebviewOptions_0] 295 | class window_RegisterCustomEditorProviderOptions_0(TypedDict): 296 | webviewOptions: NotRequired['WebviewPanelOptions'] 297 | supportsMultipleEditorsPerDocument: NotRequired[bool] 298 | 299 | class window: 300 | @singledispatchmethod 301 | def createOutputChannel(self, arg1) -> 'OutputChannel': 302 | raise TypeError(f'First argument {arg1} of single-dispatch function/method {name} has illegal type {type(arg1)}') 303 | 304 | @createOutputChannel.register 305 | def _(name: str, languageId: Optional[str] = None) -> 'OutputChannel': 306 | pass 307 | 308 | @createOutputChannel.register 309 | def _(name: str, options: window_CreateOutputChannelOptions_0) -> 'LogOutputChannel': 310 | pass 311 | 312 | def createWebviewPanel(viewType: str, title: str, showOptions: 'ViewColumn | window_CreateWebviewPanelShowOptions_1', options: Optional[Any] = None) -> 'WebviewPanel': 313 | pass 314 | 315 | def registerWebviewViewProvider(viewId: str, provider: 'WebviewViewProvider', options: Optional[window_RegisterWebviewViewProviderOptions_0] = None) -> 'Disposable': 316 | pass 317 | 318 | def registerCustomEditorProvider(viewType: str, provider: 'CustomTextEditorProvider | CustomReadonlyEditorProvider | CustomEditorProvider', options: Optional[window_RegisterCustomEditorProviderOptions_0] = None) -> 'Disposable': 319 | pass 320 | onDidChangeActiveColorTheme: 'Event[ColorTheme]'""" 321 | -------------------------------------------------------------------------------- /ts2python/typeddict_shim.py: -------------------------------------------------------------------------------- 1 | """typeddict_shim.py - A version of typed-dict that supports 2 | Required / NotRequired -fields across all version of Python 3 | from 3.6 onward. 4 | 5 | Until Python version 3.10, the STL's TypeDict merely supports 6 | classifying all fields of a TypedDict class as either required 7 | or optional. This TypedDict implementation classifies allows 8 | to mark fields as "NotRequired[...]". Presently, "NotRequired" 9 | is implemented as just another name for "Optional" and 10 | "Optional[...]" as well as its synonym "Union[..., None] are 11 | interpreted as allowing to leave a field out. Thins runs 12 | contrary the standard semantics of "Optional" as well as PEP 655, 13 | but should - most of the time - not lead to any problems in 14 | practice.". 15 | 16 | Starting with Python 3.11 the NotRequired-marker 17 | provided by the typing-module of the STL will be used. 18 | 19 | Starting with Python 3.14 "Optional" will not be interpreted 20 | as a chiffre for NotRequired any more. (This means that 21 | old code produced by ts2python might behave differently. 22 | In particular, objects with not required fields might 23 | consume more memory than necessary, because these fields 24 | will always be present as optional types. 25 | 26 | Copyright 2022 by Eckhart Arnold (arnold@badw.de) 27 | Bavarian Academy of Sciences an Humanities (badw.de) 28 | 29 | Licensed under the Apache License, Version 2.0 (the "License"); 30 | you may not use this file except in compliance with the License. 31 | You may obtain a copy of the License at 32 | 33 | http://www.apache.org/licenses/LICENSE-2.0 34 | 35 | Unless required by applicable law or agreed to in writing, software 36 | distributed under the License is distributed on an "AS IS" BASIS, 37 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 38 | implied. See the License for the specific language governing 39 | permissions and limitations under the License. 40 | """ 41 | 42 | import sys 43 | 44 | __all__ = ['NotRequired', 'TypedDict', 'GenericTypedDict', '_TypedDictMeta', 45 | 'GenericMeta', 'get_origin', 'get_args', 'Literal', 'is_typeddict', 46 | 'ForwardRef', '_GenericAlias', 'ReadOnly', 'TypeAlias'] 47 | 48 | if sys.version_info >= (3, 14): 49 | from typing import (NotRequired, TypedDict, _TypedDictMeta, 50 | ForwardRef, _GenericAlias, get_origin, get_args, 51 | Literal, is_typeddict, Union, ReadOnly, TypeAlias) 52 | GenericTypedDict = TypedDict 53 | GenericMeta = type 54 | 55 | else: 56 | from typing import (Generic, TypeVar, ClassVar, Any, NoReturn, _SpecialForm, 57 | _GenericAlias, ForwardRef, Union) 58 | 59 | try: 60 | from typing import Protocol, Final 61 | except ImportError: 62 | Protocol = Generic 63 | Final = type 64 | 65 | if sys.version_info >= (3, 11): 66 | from typing import (TypedDict, _TypedDictMeta, get_origin, get_args, 67 | NotRequired, Literal, TypeAlias) 68 | if sys.version_info >= (3,13): 69 | from typing import ReadOnly 70 | else: 71 | ReadOnly = Union 72 | GenericTypedDict = TypedDict 73 | GenericMeta = type 74 | 75 | else: 76 | if sys.version_info >= (3, 10): 77 | from typing import TypeAlias 78 | else: 79 | TypeAlias = Any 80 | 81 | if sys.version_info >= (3, 8): 82 | from typing import get_args, get_origin, Optional, Literal 83 | else: 84 | from typing import Optional 85 | from DHParser.externallibs.typing_extensions import (get_origin, 86 | get_args, Literal) 87 | 88 | NotRequired = Optional 89 | ReadOnly = Union 90 | 91 | # The following functions have been copied from the Python 92 | # standard libraries typing-module. They have been adapted 93 | # to support a more flexible version of TypedDict 94 | # see also: 95 | 96 | def _type_convert(arg, module=None): 97 | """For converting None to type(None), and strings to ForwardRef.""" 98 | if arg is None: 99 | return type(None) 100 | if isinstance(arg, str): 101 | fwref = ForwardRef(arg) 102 | if hasattr(fwref, '__forward_module__'): 103 | fwref.__forward_module__ = module 104 | return fwref 105 | return arg 106 | 107 | 108 | def _type_check(arg, msg, is_argument=True, module=None): 109 | """Check that the argument is a type, and return it (internal helper). 110 | As a special case, accept None and return type(None) instead. Also wrap strings 111 | into ForwardRef instances. Consider several corner cases. For example, plain 112 | special forms like Union are not valid, while Union[int, str] is OK, etc. 113 | The msg argument is a human-readable error message, e.g.:: 114 | "Union[arg, ...]: arg should be a type." 115 | We append the repr() of the actual value (truncated to 100 chars). 116 | """ 117 | invalid_generic_forms = (Generic, Protocol) 118 | if is_argument: 119 | invalid_generic_forms = invalid_generic_forms + (ClassVar, Final) 120 | 121 | arg = _type_convert(arg, module=module) 122 | # if (isinstance(arg, _GenericAlias) and 123 | # arg.__origin__ in invalid_generic_forms): 124 | # raise TypeError(f"{arg} is not valid as type argument") 125 | if arg in (Any, NoReturn): 126 | return arg 127 | if (sys.version_info >= (3, 7) and isinstance(arg, _SpecialForm)) \ 128 | or arg in (Generic, Protocol): 129 | raise TypeError(f"Plain {arg} is not valid as type argument") 130 | if isinstance(arg, (type, TypeVar, ForwardRef)): 131 | return arg 132 | if sys.version_info >= (3, 10): 133 | from types import UnionType 134 | if isinstance(arg, UnionType): 135 | return arg 136 | if not callable(arg): 137 | print(sys.version_info, sys.version_info >= (3, 9)) 138 | raise TypeError(f"{msg} Got {arg!r:.100}.") 139 | return arg 140 | 141 | 142 | class _TypedDictMeta(type): 143 | def __new__(cls, name, bases, ns, total=True): 144 | """Create new typed dict class object. 145 | 146 | This method is called when TypedDict is subclassed, 147 | or when TypedDict is instantiated. This way 148 | TypedDict supports all three syntax forms described in its docstring. 149 | Subclasses and instances of TypedDict return actual dictionaries. 150 | """ 151 | for base in bases: 152 | if type(base) is not _TypedDictMeta and base is not Generic: 153 | raise TypeError('cannot inherit from both a TypedDict type ' 154 | 'and a non-TypedDict base class') 155 | tp_dict = type.__new__(_TypedDictMeta, name, (dict,), ns) 156 | 157 | annotations = {} 158 | own_annotations = ns.get('__annotations__', {}) 159 | # own_annotation_keys = set(own_annotations.keys()) 160 | msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" 161 | own_annotations = { 162 | n: _type_check(tp, msg, module=tp_dict.__module__) 163 | for n, tp in own_annotations.items() 164 | } 165 | required_keys = set() 166 | optional_keys = set() 167 | 168 | for base in bases: 169 | annotations.update(base.__dict__.get('__annotations__', {})) 170 | required_keys.update(base.__dict__.get('__required_keys__', ())) 171 | optional_keys.update(base.__dict__.get('__optional_keys__', ())) 172 | 173 | annotations.update(own_annotations) 174 | 175 | total = True 176 | for field, field_type in own_annotations.items(): 177 | field_type_origin = get_origin(field_type) 178 | if (field_type_origin is NotRequired 179 | or (field_type_origin is Union 180 | and type(None) in field_type.__args__) 181 | or (isinstance(field_type, ForwardRef) 182 | and (field_type.__forward_arg__.startswith('Optional[') 183 | or field_type.__forward_arg__.startswith('NotRequired') 184 | or (field_type.__forward_arg__.startswith('Union[') 185 | and field_type.__forward_arg__.endswith(', None]'))))): 186 | optional_keys.add(field) 187 | total = False 188 | else: 189 | required_keys.add(field) 190 | 191 | tp_dict.__annotations__ = annotations 192 | tp_dict.__required_keys__ = frozenset(required_keys) 193 | tp_dict.__optional_keys__ = frozenset(optional_keys) 194 | if not hasattr(tp_dict, '__total__'): 195 | tp_dict.__total__ = total 196 | return tp_dict 197 | 198 | def __getitem__(self, *args, **kwargs): 199 | pass 200 | 201 | __call__ = dict # static method 202 | 203 | def __subclasscheck__(cls, other): 204 | # Typed dicts are only for static structural subtyping. 205 | raise TypeError('TypedDict does not support instance and class checks') 206 | 207 | __instancecheck__ = __subclasscheck__ 208 | 209 | 210 | def TypedDict(typename, fields=None, *, total=True, **kwargs): 211 | """A simple typed namespace. At runtime it is equivalent to a plain dict. 212 | 213 | TypedDict creates a dictionary type that expects all of its 214 | instances to have a certain set of keys, where each key is 215 | associated with a value of a consistent type. This expectation 216 | is not checked at runtime but is only enforced by type checkers. 217 | Usage:: 218 | 219 | class Point2D(TypedDict): 220 | x: int 221 | y: int 222 | label: str 223 | 224 | a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK 225 | b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check 226 | 227 | assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first') 228 | 229 | The type info can be accessed via the Point2D.__annotations__ dict, and 230 | the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets. 231 | TypedDict supports two additional equivalent forms:: 232 | 233 | Point2D = TypedDict('Point2D', x=int, y=int, label=str) 234 | Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) 235 | 236 | By default, all keys must be present in a TypedDict. It is possible 237 | to override this by specifying totality. 238 | Usage:: 239 | 240 | class point2D(TypedDict, total=False): 241 | x: int 242 | y: int 243 | 244 | This means that a point2D TypedDict can have any of the keys omitted.A type 245 | checker is only expected to support a literal False or True as the value of 246 | the total argument. True is the default, and makes all items defined in the 247 | class body be required. 248 | 249 | The class syntax is only supported in Python 3.6+, while two other 250 | syntax forms work for Python 2.7 and 3.2+ 251 | """ 252 | if fields is None: 253 | fields = kwargs 254 | elif kwargs: 255 | raise TypeError("TypedDict takes either a dict or keyword arguments," 256 | " but not both") 257 | 258 | ns = {'__annotations__': dict(fields)} 259 | try: 260 | # Setting correct module is necessary to make typed dict classes pickleable. 261 | ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__') 262 | except (AttributeError, ValueError): 263 | pass 264 | 265 | return _TypedDictMeta(typename, (), ns, total=total) 266 | 267 | _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {}) 268 | TypedDict.__mro_entries__ = lambda bases: (_TypedDict,) 269 | GenericTypedDict = TypedDict 270 | GenericMeta = type 271 | 272 | try: 273 | from typing import _TypedDictMeta as _typing_TypedDictMeta 274 | typing_TDM_flag = True # _typing_TypedDictMeta is not the same as _TypedDictMeta 275 | except (ImportError, ModuleNotFoundError): 276 | _typing_TypedDictMeta = _TypedDictMeta 277 | typing_TDM_flag = False # _typing_TypedDictMeta is the same as _TypedDictMeta 278 | 279 | 280 | def is_typeddict(typ) -> bool: 281 | return isinstance(typ, _typing_TypedDictMeta) \ 282 | or (typing_TDM_flag and isinstance(typ, _TypedDictMeta)) --------------------------------------------------------------------------------