typesync 0.0.1a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- typesync-0.0.1a1/LICENSE +7 -0
- typesync-0.0.1a1/PKG-INFO +94 -0
- typesync-0.0.1a1/README.md +79 -0
- typesync-0.0.1a1/pyproject.toml +82 -0
- typesync-0.0.1a1/setup.cfg +4 -0
- typesync-0.0.1a1/tests/test_arg_types.py +60 -0
- typesync-0.0.1a1/tests/test_inference.py +131 -0
- typesync-0.0.1a1/tests/test_nested_types.py +118 -0
- typesync-0.0.1a1/tests/test_return_types.py +77 -0
- typesync-0.0.1a1/tests/test_stubs.py +28 -0
- typesync-0.0.1a1/tests/test_typed_dict.py +61 -0
- typesync-0.0.1a1/typesync/__init__.py +13 -0
- typesync-0.0.1a1/typesync/cli.py +126 -0
- typesync-0.0.1a1/typesync/codegen/__init__.py +7 -0
- typesync-0.0.1a1/typesync/codegen/extractor.py +232 -0
- typesync-0.0.1a1/typesync/codegen/inference.py +239 -0
- typesync-0.0.1a1/typesync/codegen/writer.py +195 -0
- typesync-0.0.1a1/typesync/ts_types.py +125 -0
- typesync-0.0.1a1/typesync/type_translators/__init__.py +12 -0
- typesync-0.0.1a1/typesync/type_translators/abstract.py +21 -0
- typesync-0.0.1a1/typesync/type_translators/base_translator.py +149 -0
- typesync-0.0.1a1/typesync/type_translators/flask_translator.py +25 -0
- typesync-0.0.1a1/typesync/type_translators/type_node.py +84 -0
- typesync-0.0.1a1/typesync/utils.py +66 -0
- typesync-0.0.1a1/typesync.egg-info/PKG-INFO +94 -0
- typesync-0.0.1a1/typesync.egg-info/SOURCES.txt +27 -0
- typesync-0.0.1a1/typesync.egg-info/dependency_links.txt +1 -0
- typesync-0.0.1a1/typesync.egg-info/requires.txt +3 -0
- typesync-0.0.1a1/typesync.egg-info/top_level.txt +1 -0
typesync-0.0.1a1/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 Francisco rodrigues
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: typesync
|
|
3
|
+
Version: 0.0.1a1
|
|
4
|
+
Summary: Auto-generate TypeScript client code from Flask routes and Python type annotations.
|
|
5
|
+
Author-email: Francisco Rodrigues <francisco.rodrigues0908@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/ArmindoFlores/typesync
|
|
7
|
+
Project-URL: GitHub, https://github.com/ArmindoFlores/typesync
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: click
|
|
12
|
+
Requires-Dist: flask
|
|
13
|
+
Requires-Dist: inflection>=0.5.1
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# Typesync
|
|
17
|
+
|
|
18
|
+
This project aims to automatically generate TypeScript types and client-side request helpers directly from a Flask application. It is heavily inspired by [JS Flask URLs](https://github.com/indico/js-flask-urls).
|
|
19
|
+
|
|
20
|
+
By inspecting Flask routes and their Python type annotations, it produces strongly typed TypeScript definitions and functions that can call those endpoints with correct argument and return types. The project is currently incomplete, but the core idea and basic functionality are already in place.
|
|
21
|
+
|
|
22
|
+
The main goal is to reduce duplication and type mismatches between backend and frontend codebases by treating the Flask app as the single source of truth for API structure and typing.
|
|
23
|
+
|
|
24
|
+
## Intended and Existing Features
|
|
25
|
+
|
|
26
|
+
Some features are already implemented, others are planned.
|
|
27
|
+
|
|
28
|
+
- [x] Parse Flask routes and URL rules
|
|
29
|
+
- [x] Parse route argument types from annotations
|
|
30
|
+
- [x] Parse return types from annotated view functions
|
|
31
|
+
- [x] Generate TypeScript type definitions
|
|
32
|
+
- [x] Generate TypeScript request/helper functions for endpoints
|
|
33
|
+
- [x] CLI integration with Flask via a custom command
|
|
34
|
+
- [x] Vite support
|
|
35
|
+
- [x] Handling of Flask converters (custom and built-in)
|
|
36
|
+
- [x] Support for multiple HTTP methods (GET, POST, PUT, DELETE, etc.)
|
|
37
|
+
- [x] Support for JSON request bodies with typed parameters
|
|
38
|
+
- [ ] Support validators such as pydantic
|
|
39
|
+
- [ ] Support for `typing.Annotated` to:
|
|
40
|
+
- [ ] Ignore specific routes
|
|
41
|
+
- [ ] Customize generation behavior (naming, visibility, etc.)
|
|
42
|
+
- [ ] Improved error reporting for unsupported or ambiguous annotations
|
|
43
|
+
- [ ] Optional generation modes (types only, requests only)
|
|
44
|
+
- [ ] Configuration file support
|
|
45
|
+
- [x] Support custom formatting for generated code
|
|
46
|
+
- [x] Handle recursive types (such as `type RecursiveType = tuple[int, RecursiveType]`)*
|
|
47
|
+
- [x] Support returning with `jsonify(...)`
|
|
48
|
+
- [x] Support extensions via translators
|
|
49
|
+
|
|
50
|
+
\* Not all cases are supported.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
The project is not yet published on PyPI. Installation is currently done directly from source, preferably using `uv`.
|
|
56
|
+
|
|
57
|
+
Clone the repository and install dependencies:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/ArmindoFlores/typesync
|
|
61
|
+
cd typesync
|
|
62
|
+
uv sync --dev
|
|
63
|
+
uv pip install -e .
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
### TypeScript generation
|
|
68
|
+
The tool is exposed as a Flask CLI command. Inside your Flask application environment, run:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
flask typesync generate OUT_DIR
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This command will load the Flask app, inspect the URL map and registered view functions, and generate the corresponding TypeScript files (types and request helpers), placing them inside `OUT_DIR`. The names of the generated files, types, and functions can be customized using command line options. For more information about these options, use `flask typesync generate --help`.
|
|
75
|
+
|
|
76
|
+
### Using the generated code
|
|
77
|
+
The main output of typesync is a `makeAPI()` function that is used to instantiate an object containing a function per HTTP method per endpoint. An example on how to use this function is provided in [example/frontend/src/api.ts](example/frontend/src/api.ts).
|
|
78
|
+
|
|
79
|
+
### Rollup Plugin
|
|
80
|
+
Typesync has a rollup plugin that can be used to integrate with Rollup/Vite projects. Additional documentation is provided in [rollup-plugin-typesync/README.md](rollup-plugin-typesync/README.md).
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## Inference
|
|
84
|
+
|
|
85
|
+
Typesync is capable of some basic type inference. This can be helpful when trying to incrementally adopt this package in an existing codebase, or for unconventional Flask setups. This functionality is optional, and needs to be enabled using the `--inference` flag. Additionally, the inference module may need to use `eval()` for evaluating some types; this is disabled by default, but can be enabled using `--inference-can-eval`.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## Running Tests
|
|
89
|
+
|
|
90
|
+
Tests can be run using pytest:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pytest
|
|
94
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Typesync
|
|
2
|
+
|
|
3
|
+
This project aims to automatically generate TypeScript types and client-side request helpers directly from a Flask application. It is heavily inspired by [JS Flask URLs](https://github.com/indico/js-flask-urls).
|
|
4
|
+
|
|
5
|
+
By inspecting Flask routes and their Python type annotations, it produces strongly typed TypeScript definitions and functions that can call those endpoints with correct argument and return types. The project is currently incomplete, but the core idea and basic functionality are already in place.
|
|
6
|
+
|
|
7
|
+
The main goal is to reduce duplication and type mismatches between backend and frontend codebases by treating the Flask app as the single source of truth for API structure and typing.
|
|
8
|
+
|
|
9
|
+
## Intended and Existing Features
|
|
10
|
+
|
|
11
|
+
Some features are already implemented, others are planned.
|
|
12
|
+
|
|
13
|
+
- [x] Parse Flask routes and URL rules
|
|
14
|
+
- [x] Parse route argument types from annotations
|
|
15
|
+
- [x] Parse return types from annotated view functions
|
|
16
|
+
- [x] Generate TypeScript type definitions
|
|
17
|
+
- [x] Generate TypeScript request/helper functions for endpoints
|
|
18
|
+
- [x] CLI integration with Flask via a custom command
|
|
19
|
+
- [x] Vite support
|
|
20
|
+
- [x] Handling of Flask converters (custom and built-in)
|
|
21
|
+
- [x] Support for multiple HTTP methods (GET, POST, PUT, DELETE, etc.)
|
|
22
|
+
- [x] Support for JSON request bodies with typed parameters
|
|
23
|
+
- [ ] Support validators such as pydantic
|
|
24
|
+
- [ ] Support for `typing.Annotated` to:
|
|
25
|
+
- [ ] Ignore specific routes
|
|
26
|
+
- [ ] Customize generation behavior (naming, visibility, etc.)
|
|
27
|
+
- [ ] Improved error reporting for unsupported or ambiguous annotations
|
|
28
|
+
- [ ] Optional generation modes (types only, requests only)
|
|
29
|
+
- [ ] Configuration file support
|
|
30
|
+
- [x] Support custom formatting for generated code
|
|
31
|
+
- [x] Handle recursive types (such as `type RecursiveType = tuple[int, RecursiveType]`)*
|
|
32
|
+
- [x] Support returning with `jsonify(...)`
|
|
33
|
+
- [x] Support extensions via translators
|
|
34
|
+
|
|
35
|
+
\* Not all cases are supported.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
The project is not yet published on PyPI. Installation is currently done directly from source, preferably using `uv`.
|
|
41
|
+
|
|
42
|
+
Clone the repository and install dependencies:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/ArmindoFlores/typesync
|
|
46
|
+
cd typesync
|
|
47
|
+
uv sync --dev
|
|
48
|
+
uv pip install -e .
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
### TypeScript generation
|
|
53
|
+
The tool is exposed as a Flask CLI command. Inside your Flask application environment, run:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
flask typesync generate OUT_DIR
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This command will load the Flask app, inspect the URL map and registered view functions, and generate the corresponding TypeScript files (types and request helpers), placing them inside `OUT_DIR`. The names of the generated files, types, and functions can be customized using command line options. For more information about these options, use `flask typesync generate --help`.
|
|
60
|
+
|
|
61
|
+
### Using the generated code
|
|
62
|
+
The main output of typesync is a `makeAPI()` function that is used to instantiate an object containing a function per HTTP method per endpoint. An example on how to use this function is provided in [example/frontend/src/api.ts](example/frontend/src/api.ts).
|
|
63
|
+
|
|
64
|
+
### Rollup Plugin
|
|
65
|
+
Typesync has a rollup plugin that can be used to integrate with Rollup/Vite projects. Additional documentation is provided in [rollup-plugin-typesync/README.md](rollup-plugin-typesync/README.md).
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Inference
|
|
69
|
+
|
|
70
|
+
Typesync is capable of some basic type inference. This can be helpful when trying to incrementally adopt this package in an existing codebase, or for unconventional Flask setups. This functionality is optional, and needs to be enabled using the `--inference` flag. Additionally, the inference module may need to use `eval()` for evaluating some types; this is disabled by default, but can be enabled using `--inference-can-eval`.
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
## Running Tests
|
|
74
|
+
|
|
75
|
+
Tests can be run using pytest:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pytest
|
|
79
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
version = "0.0.1a1"
|
|
3
|
+
name = "typesync"
|
|
4
|
+
description = "Auto-generate TypeScript client code from Flask routes and Python type annotations."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license-files = ["LICENSE"]
|
|
7
|
+
dependencies = [
|
|
8
|
+
"click",
|
|
9
|
+
"flask",
|
|
10
|
+
"inflection>=0.5.1",
|
|
11
|
+
]
|
|
12
|
+
requires-python = ">= 3.12"
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Francisco Rodrigues", email = "francisco.rodrigues0908@gmail.com"},
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/ArmindoFlores/typesync"
|
|
19
|
+
GitHub = "https://github.com/ArmindoFlores/typesync"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["setuptools>=61"]
|
|
23
|
+
build-backend = "setuptools.build_meta"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["."]
|
|
27
|
+
include = ["typesync*"]
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = [
|
|
31
|
+
"ty>=0.0.11",
|
|
32
|
+
"Flask-CORS",
|
|
33
|
+
"pytest>=9.0.2",
|
|
34
|
+
"types-flask-cors>=6.0.0.20250809",
|
|
35
|
+
"pre-commit>=4.5.1",
|
|
36
|
+
"ruff>=0.14.13",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.ruff.lint]
|
|
40
|
+
extend-select = [
|
|
41
|
+
"E", # pycodestyle (errors)
|
|
42
|
+
"W", # pycodestyle (warnings)
|
|
43
|
+
"F", # pyflakes
|
|
44
|
+
"N", # pep8-naming
|
|
45
|
+
"Q", # flake8-quotes
|
|
46
|
+
"RUF", # ruff
|
|
47
|
+
"UP", # pyupgrade
|
|
48
|
+
"S", # flake8-bandit
|
|
49
|
+
"C4", # flake8-comprehensions
|
|
50
|
+
"INT", # flake8-gettext
|
|
51
|
+
"LOG", # flake8-logging
|
|
52
|
+
"G", # flake8-logging-format
|
|
53
|
+
"B", # flake8-bugbear
|
|
54
|
+
"A001", # flake8-builtins
|
|
55
|
+
"COM", # flake8-commas
|
|
56
|
+
"T10", # flake8-debugger
|
|
57
|
+
"EXE", # flake8-executable
|
|
58
|
+
"ISC", # flake8-implicit-str-concat
|
|
59
|
+
"PIE", # flake8-pie
|
|
60
|
+
"PT", # flake8-pytest-style
|
|
61
|
+
"RSE", # flake8-raise
|
|
62
|
+
"RET504", # flake8-return
|
|
63
|
+
"SIM", # flake8-simplify
|
|
64
|
+
"TID", # flake8-tidy-imports
|
|
65
|
+
"PGH", # pygrep-hooks
|
|
66
|
+
"PL", # pylint
|
|
67
|
+
"TRY", # tryceratops
|
|
68
|
+
"PERF", # perflint
|
|
69
|
+
"FURB", # refurb
|
|
70
|
+
]
|
|
71
|
+
extend-ignore = [
|
|
72
|
+
"PLR0911",
|
|
73
|
+
"PLR0913",
|
|
74
|
+
"PLR2004",
|
|
75
|
+
"COM812",
|
|
76
|
+
"RET504",
|
|
77
|
+
"G004",
|
|
78
|
+
"TRY400",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint.per-file-ignores]
|
|
82
|
+
"tests/*.py" = ["S101"]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from flask import Flask
|
|
2
|
+
|
|
3
|
+
from typesync.ts_types import TSObject, TSSimpleType
|
|
4
|
+
|
|
5
|
+
from conftest import ParserFixture
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_args_int(app: Flask, args_parser: ParserFixture) -> None:
|
|
9
|
+
@app.route("/<int:test>")
|
|
10
|
+
def main(test) -> list:
|
|
11
|
+
return []
|
|
12
|
+
|
|
13
|
+
assert args_parser(app, "main") == TSObject(
|
|
14
|
+
("test",),
|
|
15
|
+
(TSSimpleType("number"),),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_args_float(app: Flask, args_parser: ParserFixture) -> None:
|
|
20
|
+
@app.route("/<float:test>")
|
|
21
|
+
def main(test) -> list:
|
|
22
|
+
return []
|
|
23
|
+
|
|
24
|
+
assert args_parser(app, "main") == TSObject(
|
|
25
|
+
("test",),
|
|
26
|
+
(TSSimpleType("number"),),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_args_path(app: Flask, args_parser: ParserFixture) -> None:
|
|
31
|
+
@app.route("/<path:test>")
|
|
32
|
+
def main(test) -> list:
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
assert args_parser(app, "main") == TSObject(
|
|
36
|
+
("test",),
|
|
37
|
+
(TSSimpleType("string"),),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_args_uuid(app: Flask, args_parser: ParserFixture) -> None:
|
|
42
|
+
@app.route("/<uuid:test>")
|
|
43
|
+
def main(test) -> list:
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
assert args_parser(app, "main") == TSObject(
|
|
47
|
+
("test",),
|
|
48
|
+
(TSSimpleType("string"),),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_args_unspecified(app: Flask, args_parser: ParserFixture) -> None:
|
|
53
|
+
@app.route("/<test>")
|
|
54
|
+
def main(test) -> list:
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
assert args_parser(app, "main") == TSObject(
|
|
58
|
+
("test",),
|
|
59
|
+
(TSSimpleType("string"),),
|
|
60
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from flask import Flask
|
|
3
|
+
|
|
4
|
+
from typesync.ts_types import TSArray, TSRecord, TSSimpleType
|
|
5
|
+
|
|
6
|
+
from conftest import ParserFixture
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_literal_string(app: Flask, inf_return_parser: ParserFixture) -> None:
|
|
10
|
+
@app.route("/main")
|
|
11
|
+
def main():
|
|
12
|
+
return "hello"
|
|
13
|
+
|
|
14
|
+
assert inf_return_parser(app, "main") == TSSimpleType("string")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_literal_int(app: Flask, inf_return_parser: ParserFixture) -> None:
|
|
18
|
+
@app.route("/main")
|
|
19
|
+
def main():
|
|
20
|
+
return 202
|
|
21
|
+
|
|
22
|
+
assert inf_return_parser(app, "main") == TSSimpleType("number")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_literal_list_same_type(app: Flask, inf_return_parser: ParserFixture) -> None:
|
|
26
|
+
@app.route("/main")
|
|
27
|
+
def main():
|
|
28
|
+
return [1, 2, 3]
|
|
29
|
+
|
|
30
|
+
assert inf_return_parser(app, "main") == TSArray(TSSimpleType("number"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_literal_list_mixed(app: Flask, inf_return_parser: ParserFixture) -> None:
|
|
34
|
+
@app.route("/main")
|
|
35
|
+
def main():
|
|
36
|
+
return [1, "hello", 3]
|
|
37
|
+
|
|
38
|
+
assert inf_return_parser(app, "main") == TSArray(TSSimpleType("any"))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_literal_tuple(app: Flask, inf_return_parser: ParserFixture) -> None:
|
|
42
|
+
@app.route("/main")
|
|
43
|
+
def main():
|
|
44
|
+
return ("hello", 404)
|
|
45
|
+
|
|
46
|
+
assert inf_return_parser(app, "main") == TSSimpleType("string")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_literal_dict_same_type(app: Flask, inf_return_parser: ParserFixture) -> None:
|
|
50
|
+
@app.route("/main")
|
|
51
|
+
def main():
|
|
52
|
+
return {
|
|
53
|
+
"key1": [1, 2, 3],
|
|
54
|
+
"key2": [0, 1],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
assert inf_return_parser(app, "main") == TSRecord(
|
|
58
|
+
TSSimpleType("string"), TSArray(TSSimpleType("number"))
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_literal_dict_mixed_v(app: Flask, inf_return_parser: ParserFixture) -> None:
|
|
63
|
+
@app.route("/main")
|
|
64
|
+
def main():
|
|
65
|
+
return {
|
|
66
|
+
"key1": [1, 2, 3],
|
|
67
|
+
"key2": "hello",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
assert inf_return_parser(app, "main") == TSRecord(
|
|
71
|
+
TSSimpleType("string"), TSSimpleType("any")
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_literal_dict_mixed_k(app: Flask, inf_return_parser: ParserFixture) -> None:
|
|
76
|
+
@app.route("/main")
|
|
77
|
+
def main():
|
|
78
|
+
return {
|
|
79
|
+
12: 1,
|
|
80
|
+
"key2": 2,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
assert inf_return_parser(app, "main") == TSRecord(
|
|
84
|
+
TSSimpleType("any"), TSSimpleType("number")
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_literal_dict_mixed(app: Flask, inf_return_parser: ParserFixture) -> None:
|
|
89
|
+
@app.route("/main")
|
|
90
|
+
def main():
|
|
91
|
+
return {
|
|
92
|
+
12: [1, 2, 3],
|
|
93
|
+
"key2": "hello",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
assert inf_return_parser(app, "main") == TSSimpleType("object")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_function_call(app: Flask, inf_return_parser: ParserFixture) -> None:
|
|
100
|
+
def result(arg: int):
|
|
101
|
+
return arg
|
|
102
|
+
|
|
103
|
+
@app.route("/main")
|
|
104
|
+
def main():
|
|
105
|
+
return result(12)
|
|
106
|
+
|
|
107
|
+
assert inf_return_parser(app, "main") == TSSimpleType("number")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_class_factory(app: Flask, inf_return_parser: ParserFixture) -> None:
|
|
111
|
+
R = typing.TypeVar("R")
|
|
112
|
+
|
|
113
|
+
class RHProtocol(typing.Protocol[R]):
|
|
114
|
+
def process(self) -> R: ...
|
|
115
|
+
|
|
116
|
+
class TestInference:
|
|
117
|
+
def process(self) -> int:
|
|
118
|
+
return 12
|
|
119
|
+
|
|
120
|
+
def make_route_function[R](
|
|
121
|
+
obj: typing.Callable[[], RHProtocol[R]],
|
|
122
|
+
) -> typing.Callable[[], R]:
|
|
123
|
+
def wrapper() -> R:
|
|
124
|
+
inst = obj()
|
|
125
|
+
return inst.process()
|
|
126
|
+
|
|
127
|
+
return wrapper
|
|
128
|
+
|
|
129
|
+
app.add_url_rule("/main", "main", make_route_function(TestInference))
|
|
130
|
+
|
|
131
|
+
assert inf_return_parser(app, "main") == TSSimpleType("number")
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from flask import Flask
|
|
2
|
+
|
|
3
|
+
from typesync.ts_types import (
|
|
4
|
+
TSArray,
|
|
5
|
+
TSSimpleType,
|
|
6
|
+
TSRecord,
|
|
7
|
+
TSTuple,
|
|
8
|
+
TSUnion,
|
|
9
|
+
TSRecursiveType,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from conftest import ParserFixture
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
type Alias1[A] = list[A]
|
|
16
|
+
type Alias2[B] = Alias1[B]
|
|
17
|
+
type AliasedArgs[A, B] = dict[str, tuple[Alias1[B], Alias2[A]]]
|
|
18
|
+
type Recursive1[A] = tuple[A | Recursive1[A], ...]
|
|
19
|
+
type Recursive2[A, B] = tuple[A | Recursive2[B, A], ...]
|
|
20
|
+
type Recursive3[A, B, C] = tuple[A | Recursive3[C, A, B], ...]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_nested_depth_1(app: Flask, return_parser: ParserFixture) -> None:
|
|
24
|
+
@app.route("/main")
|
|
25
|
+
def main() -> Alias1[str]:
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
assert return_parser(app, "main") == TSArray(TSSimpleType("string"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_nested_depth_2(app: Flask, return_parser: ParserFixture) -> None:
|
|
32
|
+
@app.route("/main")
|
|
33
|
+
def main() -> Alias2[str]:
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
assert return_parser(app, "main") == TSArray(TSSimpleType("string"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_nested_args(app: Flask, return_parser: ParserFixture) -> None:
|
|
40
|
+
@app.route("/main")
|
|
41
|
+
def main() -> AliasedArgs[int, bool]:
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
assert return_parser(app, "main") == TSRecord(
|
|
45
|
+
TSSimpleType("string"),
|
|
46
|
+
TSTuple(
|
|
47
|
+
(
|
|
48
|
+
TSArray(TSSimpleType("boolean")),
|
|
49
|
+
TSArray(TSSimpleType("number")),
|
|
50
|
+
)
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_recursive_args_1(app: Flask, return_parser: ParserFixture) -> None:
|
|
56
|
+
@app.route("/main")
|
|
57
|
+
def main() -> Recursive1[int]:
|
|
58
|
+
return ()
|
|
59
|
+
|
|
60
|
+
assert return_parser(app, "main") == TSArray(
|
|
61
|
+
TSUnion(
|
|
62
|
+
(
|
|
63
|
+
TSSimpleType("number"),
|
|
64
|
+
TSRecursiveType(),
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_recursive_args_2(app: Flask, return_parser: ParserFixture) -> None:
|
|
71
|
+
@app.route("/main")
|
|
72
|
+
def main() -> Recursive2[bool, str]:
|
|
73
|
+
return ()
|
|
74
|
+
|
|
75
|
+
assert return_parser(app, "main") == TSArray(
|
|
76
|
+
TSUnion(
|
|
77
|
+
(
|
|
78
|
+
TSSimpleType("boolean"),
|
|
79
|
+
TSArray(
|
|
80
|
+
TSUnion(
|
|
81
|
+
(
|
|
82
|
+
TSSimpleType("string"),
|
|
83
|
+
TSRecursiveType(),
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_recursive_args_3(app: Flask, return_parser: ParserFixture) -> None:
|
|
93
|
+
@app.route("/main")
|
|
94
|
+
def main() -> Recursive3[bool, str, int]:
|
|
95
|
+
return ()
|
|
96
|
+
|
|
97
|
+
assert return_parser(app, "main") == TSArray(
|
|
98
|
+
TSUnion(
|
|
99
|
+
(
|
|
100
|
+
TSSimpleType("boolean"),
|
|
101
|
+
TSArray(
|
|
102
|
+
TSUnion(
|
|
103
|
+
(
|
|
104
|
+
TSSimpleType("number"),
|
|
105
|
+
TSArray(
|
|
106
|
+
TSUnion(
|
|
107
|
+
(
|
|
108
|
+
TSSimpleType("string"),
|
|
109
|
+
TSRecursiveType(),
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from flask import Flask
|
|
2
|
+
|
|
3
|
+
from typesync.ts_types import TSArray, TSSimpleType, TSRecord
|
|
4
|
+
|
|
5
|
+
from conftest import ParserFixture
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_return_dict(app: Flask, return_parser: ParserFixture) -> None:
|
|
9
|
+
@app.route("/main")
|
|
10
|
+
def main() -> dict[str, int]:
|
|
11
|
+
return {"result": 42}
|
|
12
|
+
|
|
13
|
+
assert return_parser(app, "main") == TSRecord(
|
|
14
|
+
TSSimpleType("string"), TSSimpleType("number")
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_return_dict_complex(app: Flask, return_parser: ParserFixture) -> None:
|
|
19
|
+
@app.route("/main")
|
|
20
|
+
def main() -> dict[str, dict[int, bool]]:
|
|
21
|
+
return {"result": {1: True}}
|
|
22
|
+
|
|
23
|
+
assert return_parser(app, "main") == TSRecord(
|
|
24
|
+
TSSimpleType("string"),
|
|
25
|
+
TSRecord(
|
|
26
|
+
TSSimpleType("number"),
|
|
27
|
+
TSSimpleType("boolean"),
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_return_dict_missing_params(app: Flask, return_parser: ParserFixture) -> None:
|
|
33
|
+
@app.route("/main")
|
|
34
|
+
def main() -> dict:
|
|
35
|
+
return {"result": 42}
|
|
36
|
+
|
|
37
|
+
assert return_parser(app, "main") == TSSimpleType("object")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_return_list(app: Flask, return_parser: ParserFixture) -> None:
|
|
41
|
+
@app.route("/main")
|
|
42
|
+
def main() -> list[int]:
|
|
43
|
+
return [1, 2, 3]
|
|
44
|
+
|
|
45
|
+
assert return_parser(app, "main") == TSArray(TSSimpleType("number"))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_return_list_complex(app: Flask, return_parser: ParserFixture) -> None:
|
|
49
|
+
@app.route("/main")
|
|
50
|
+
def main() -> list[list[str]]:
|
|
51
|
+
return [[""]]
|
|
52
|
+
|
|
53
|
+
assert return_parser(app, "main") == TSArray(TSArray(TSSimpleType("string")))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_return_list_missing_params(app: Flask, return_parser: ParserFixture) -> None:
|
|
57
|
+
@app.route("/main")
|
|
58
|
+
def main() -> list:
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
assert return_parser(app, "main") == TSArray(TSSimpleType("any"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_return_tuple1(app: Flask, return_parser: ParserFixture) -> None:
|
|
65
|
+
@app.route("/main")
|
|
66
|
+
def main() -> tuple[str, int]:
|
|
67
|
+
return ("Status", 200)
|
|
68
|
+
|
|
69
|
+
assert return_parser(app, "main") == TSSimpleType("string")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_return_tuple2(app: Flask, return_parser: ParserFixture) -> None:
|
|
73
|
+
@app.route("/main")
|
|
74
|
+
def main() -> tuple[str, int, tuple]:
|
|
75
|
+
return ("Status", 200, ())
|
|
76
|
+
|
|
77
|
+
assert return_parser(app, "main") == TSSimpleType("string")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from flask import Flask
|
|
2
|
+
|
|
3
|
+
from typesync.ts_types import TSSimpleType, TSTuple
|
|
4
|
+
from typesync.utils import Response, jsonify
|
|
5
|
+
|
|
6
|
+
from conftest import ParserFixture
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_jsonify_none(app: Flask, return_parser: ParserFixture) -> None:
|
|
10
|
+
@app.route("/main")
|
|
11
|
+
def main() -> Response[None]:
|
|
12
|
+
return jsonify(None)
|
|
13
|
+
|
|
14
|
+
assert return_parser(app, "main") == TSSimpleType("null")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_jsonify_tuple(app: Flask, return_parser: ParserFixture) -> None:
|
|
18
|
+
@app.route("/main")
|
|
19
|
+
def main() -> Response[tuple[int, str, int]]:
|
|
20
|
+
return jsonify((1, "hello", 4))
|
|
21
|
+
|
|
22
|
+
assert return_parser(app, "main") == TSTuple(
|
|
23
|
+
(
|
|
24
|
+
TSSimpleType("number"),
|
|
25
|
+
TSSimpleType("string"),
|
|
26
|
+
TSSimpleType("number"),
|
|
27
|
+
)
|
|
28
|
+
)
|