betterproto2-compiler 0.2.0__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- betterproto2_compiler-0.2.0/LICENSE.md +22 -0
- betterproto2_compiler-0.2.0/PKG-INFO +35 -0
- betterproto2_compiler-0.2.0/README.md +11 -0
- betterproto2_compiler-0.2.0/pyproject.toml +155 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/__init__.py +0 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/casing.py +140 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/compile/__init__.py +0 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/compile/importing.py +180 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/compile/naming.py +21 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/known_types/__init__.py +14 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/known_types/any.py +36 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/known_types/duration.py +25 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/known_types/timestamp.py +45 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/lib/__init__.py +0 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/lib/google/__init__.py +0 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/lib/google/protobuf/__init__.py +3338 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/lib/google/protobuf/compiler/__init__.py +235 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/lib/message_pool.py +3 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/plugin/__init__.py +3 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/plugin/__main__.py +3 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/plugin/compiler.py +70 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/plugin/main.py +47 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/plugin/models.py +643 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/plugin/module_validation.py +156 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/plugin/parser.py +272 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/plugin/plugin.bat +2 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/plugin/typing_compiler.py +163 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/py.typed +0 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/settings.py +9 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/templates/header.py.j2 +59 -0
- betterproto2_compiler-0.2.0/src/betterproto2_compiler/templates/template.py.j2 +258 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2023 Daniel G. Taylor
|
4
|
+
Copyright (c) 2024 The betterproto contributors
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
8
|
+
in the Software without restriction, including without limitation the rights
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
11
|
+
furnished to do so, subject to the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
14
|
+
copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22
|
+
SOFTWARE.
|
@@ -0,0 +1,35 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: betterproto2_compiler
|
3
|
+
Version: 0.2.0
|
4
|
+
Summary: Compiler for betterproto2
|
5
|
+
License: MIT
|
6
|
+
Keywords: protobuf,gRPC,compiler
|
7
|
+
Author: Adrien Vannson
|
8
|
+
Author-email: adrien.vannson@protonmail.com
|
9
|
+
Requires-Python: >=3.10,<4.0
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
16
|
+
Requires-Dist: betterproto2 (>=0.2.1,<0.3.0)
|
17
|
+
Requires-Dist: grpclib (>=0.4.1,<0.5.0)
|
18
|
+
Requires-Dist: jinja2 (>=3.0.3)
|
19
|
+
Requires-Dist: ruff (>=0.7.4,<0.8.0)
|
20
|
+
Requires-Dist: typing-extensions (>=4.7.1,<5.0.0)
|
21
|
+
Project-URL: Repository, https://github.com/betterproto/python-betterproto2-compiler
|
22
|
+
Description-Content-Type: text/markdown
|
23
|
+
|
24
|
+
# Betterproto2 compiler
|
25
|
+
|
26
|
+
![](https://github.com/betterproto/python-betterproto2-compiler/actions/workflows/ci.yml/badge.svg)
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
## License
|
31
|
+
|
32
|
+
Copyright © 2019 Daniel G. Taylor
|
33
|
+
|
34
|
+
Copyright © 2024 The betterproto contributors
|
35
|
+
|
@@ -0,0 +1,155 @@
|
|
1
|
+
[tool.poetry]
|
2
|
+
name = "betterproto2_compiler"
|
3
|
+
version = "0.2.0"
|
4
|
+
description = "Compiler for betterproto2"
|
5
|
+
authors = ["Adrien Vannson <adrien.vannson@protonmail.com>", "Daniel G. Taylor <danielgtaylor@gmail.com>"]
|
6
|
+
readme = "README.md"
|
7
|
+
repository = "https://github.com/betterproto/python-betterproto2-compiler"
|
8
|
+
keywords = ["protobuf", "gRPC", "compiler"]
|
9
|
+
license = "MIT"
|
10
|
+
packages = [
|
11
|
+
{ include = "betterproto2_compiler", from = "src" }
|
12
|
+
]
|
13
|
+
|
14
|
+
[tool.poetry.dependencies]
|
15
|
+
python = "^3.10"
|
16
|
+
betterproto2 = "^0.2.1"
|
17
|
+
# The Ruff version is pinned. To update it, also update it in .pre-commit-config.yaml
|
18
|
+
ruff = "~0.7.4"
|
19
|
+
grpclib = "^0.4.1"
|
20
|
+
jinja2 = ">=3.0.3"
|
21
|
+
typing-extensions = "^4.7.1"
|
22
|
+
|
23
|
+
[tool.poetry.group.dev.dependencies]
|
24
|
+
pre-commit = "^2.17.0"
|
25
|
+
grpcio-tools = "^1.54.2"
|
26
|
+
mkdocs-material = {version = "^9.5.49", python = ">=3.10"}
|
27
|
+
mkdocstrings = {version = "^0.27.0", python = ">=3.10", extras = ["python"]}
|
28
|
+
poethepoet = ">=0.9.0"
|
29
|
+
pyright = "^1.1.391"
|
30
|
+
ipykernel = "^6.29.5"
|
31
|
+
|
32
|
+
[tool.poetry.group.test.dependencies]
|
33
|
+
pytest = "^6.2.5"
|
34
|
+
protobuf = "^4"
|
35
|
+
|
36
|
+
[tool.poetry.scripts]
|
37
|
+
protoc-gen-python_betterproto2 = "betterproto2_compiler.plugin:main"
|
38
|
+
|
39
|
+
[tool.ruff]
|
40
|
+
extend-exclude = ["tests/output_*", "src/betterproto2_compiler/lib"]
|
41
|
+
target-version = "py310"
|
42
|
+
line-length = 120
|
43
|
+
|
44
|
+
[tool.ruff.lint]
|
45
|
+
select = [
|
46
|
+
"F401", # Unused imports
|
47
|
+
"F841", # Unused local variables
|
48
|
+
"F821", # Undefined names
|
49
|
+
"E501", # Line length violations
|
50
|
+
|
51
|
+
"SIM101", # Simplify unnecessary if-else blocks
|
52
|
+
"SIM102", # Simplify return or yield statements
|
53
|
+
"SIM103", # Simplify list/set/dict comprehensions
|
54
|
+
|
55
|
+
"UP",
|
56
|
+
|
57
|
+
"I",
|
58
|
+
]
|
59
|
+
|
60
|
+
|
61
|
+
[tool.ruff.lint.isort]
|
62
|
+
combine-as-imports = true
|
63
|
+
|
64
|
+
# Dev workflow tasks
|
65
|
+
|
66
|
+
[tool.poe.tasks.test]
|
67
|
+
cmd = "pytest"
|
68
|
+
help = "Run tests"
|
69
|
+
|
70
|
+
[tool.poe.tasks.generate]
|
71
|
+
sequence = ["_generate_tests", "_generate_tests_lib"]
|
72
|
+
help = "Generate test cases"
|
73
|
+
|
74
|
+
[tool.poe.tasks._generate_tests]
|
75
|
+
script = "tests.generate:main"
|
76
|
+
|
77
|
+
[tool.poe.tasks._generate_tests_lib]
|
78
|
+
shell = """
|
79
|
+
python -m grpc.tools.protoc \
|
80
|
+
--python_betterproto2_out=tests/output_betterproto \
|
81
|
+
google/protobuf/any.proto \
|
82
|
+
google/protobuf/api.proto \
|
83
|
+
google/protobuf/duration.proto \
|
84
|
+
google/protobuf/empty.proto \
|
85
|
+
google/protobuf/field_mask.proto \
|
86
|
+
google/protobuf/source_context.proto \
|
87
|
+
google/protobuf/struct.proto \
|
88
|
+
google/protobuf/timestamp.proto \
|
89
|
+
google/protobuf/type.proto \
|
90
|
+
google/protobuf/wrappers.proto
|
91
|
+
|
92
|
+
python -m grpc.tools.protoc \
|
93
|
+
--python_betterproto2_out=tests/output_betterproto_pydantic \
|
94
|
+
--python_betterproto2_opt=pydantic_dataclasses \
|
95
|
+
google/protobuf/any.proto \
|
96
|
+
google/protobuf/api.proto \
|
97
|
+
google/protobuf/duration.proto \
|
98
|
+
google/protobuf/empty.proto \
|
99
|
+
google/protobuf/field_mask.proto \
|
100
|
+
google/protobuf/source_context.proto \
|
101
|
+
google/protobuf/struct.proto \
|
102
|
+
google/protobuf/timestamp.proto \
|
103
|
+
google/protobuf/type.proto \
|
104
|
+
google/protobuf/wrappers.proto
|
105
|
+
"""
|
106
|
+
|
107
|
+
[tool.poe.tasks.typecheck]
|
108
|
+
cmd = "pyright src"
|
109
|
+
help = "Typecheck the code with Pyright"
|
110
|
+
|
111
|
+
[tool.poe.tasks.format]
|
112
|
+
sequence = ["_format", "_sort-imports"]
|
113
|
+
help = "Format the source code, and sort the imports"
|
114
|
+
|
115
|
+
[tool.poe.tasks.check]
|
116
|
+
sequence = ["_check-format", "_check-ruff-lint"]
|
117
|
+
help = "Check that the source code is formatted and the code passes the linter"
|
118
|
+
|
119
|
+
[tool.poe.tasks._format]
|
120
|
+
cmd = "ruff format src tests"
|
121
|
+
help = "Format the source code without sorting the imports"
|
122
|
+
|
123
|
+
[tool.poe.tasks._sort-imports]
|
124
|
+
cmd = "ruff check --select I --fix src tests"
|
125
|
+
help = "Sort the imports"
|
126
|
+
|
127
|
+
[tool.poe.tasks._check-format]
|
128
|
+
cmd = "ruff format --diff src tests"
|
129
|
+
help = "Check that the source code is formatted"
|
130
|
+
|
131
|
+
[tool.poe.tasks._check-ruff-lint]
|
132
|
+
cmd = "ruff check src tests"
|
133
|
+
help = "Check the code with the Ruff linter"
|
134
|
+
|
135
|
+
[tool.poe.tasks.serve-docs]
|
136
|
+
cmd = "mkdocs serve"
|
137
|
+
help = "Serve the documentation locally"
|
138
|
+
|
139
|
+
[build-system]
|
140
|
+
requires = ["poetry-core>=1.0.0,<2"]
|
141
|
+
build-backend = "poetry.core.masonry.api"
|
142
|
+
|
143
|
+
# python -m grpc.tools.protoc \
|
144
|
+
# --python_betterproto2_out=src/lib2 \
|
145
|
+
# google/protobuf/any.proto \
|
146
|
+
# google/protobuf/api.proto \
|
147
|
+
# google/protobuf/duration.proto \
|
148
|
+
# google/protobuf/empty.proto \
|
149
|
+
# google/protobuf/field_mask.proto \
|
150
|
+
# google/protobuf/source_context.proto \
|
151
|
+
# google/protobuf/struct.proto \
|
152
|
+
# google/protobuf/timestamp.proto \
|
153
|
+
# google/protobuf/type.proto \
|
154
|
+
# google/protobuf/wrappers.proto \
|
155
|
+
# google/protobuf/compiler/plugin.proto
|
File without changes
|
@@ -0,0 +1,140 @@
|
|
1
|
+
import keyword
|
2
|
+
import re
|
3
|
+
|
4
|
+
# Word delimiters and symbols that will not be preserved when re-casing.
|
5
|
+
# language=PythonRegExp
|
6
|
+
SYMBOLS = "[^a-zA-Z0-9]*"
|
7
|
+
|
8
|
+
# Optionally capitalized word.
|
9
|
+
# language=PythonRegExp
|
10
|
+
WORD = "[A-Z]*[a-z]*[0-9]*"
|
11
|
+
|
12
|
+
# Uppercase word, not followed by lowercase letters.
|
13
|
+
# language=PythonRegExp
|
14
|
+
WORD_UPPER = "[A-Z]+(?![a-z])[0-9]*"
|
15
|
+
|
16
|
+
|
17
|
+
def safe_snake_case(value: str) -> str:
|
18
|
+
"""Snake case a value taking into account Python keywords."""
|
19
|
+
value = snake_case(value)
|
20
|
+
value = sanitize_name(value)
|
21
|
+
return value
|
22
|
+
|
23
|
+
|
24
|
+
def snake_case(value: str, strict: bool = True) -> str:
|
25
|
+
"""
|
26
|
+
Join words with an underscore into lowercase and remove symbols.
|
27
|
+
|
28
|
+
Parameters
|
29
|
+
-----------
|
30
|
+
value: :class:`str`
|
31
|
+
The value to convert.
|
32
|
+
strict: :class:`bool`
|
33
|
+
Whether or not to force single underscores.
|
34
|
+
|
35
|
+
Returns
|
36
|
+
--------
|
37
|
+
:class:`str`
|
38
|
+
The value in snake_case.
|
39
|
+
"""
|
40
|
+
|
41
|
+
def substitute_word(symbols: str, word: str, is_start: bool) -> str:
|
42
|
+
if not word:
|
43
|
+
return ""
|
44
|
+
if strict:
|
45
|
+
delimiter_count = 0 if is_start else 1 # Single underscore if strict.
|
46
|
+
elif is_start:
|
47
|
+
delimiter_count = len(symbols)
|
48
|
+
elif word.isupper() or word.islower():
|
49
|
+
delimiter_count = max(1, len(symbols)) # Preserve all delimiters if not strict.
|
50
|
+
else:
|
51
|
+
delimiter_count = len(symbols) + 1 # Extra underscore for leading capital.
|
52
|
+
|
53
|
+
return ("_" * delimiter_count) + word.lower()
|
54
|
+
|
55
|
+
snake = re.sub(
|
56
|
+
f"(^)?({SYMBOLS})({WORD_UPPER}|{WORD})",
|
57
|
+
lambda groups: substitute_word(groups[2], groups[3], groups[1] is not None),
|
58
|
+
value,
|
59
|
+
)
|
60
|
+
return snake
|
61
|
+
|
62
|
+
|
63
|
+
def pascal_case(value: str, strict: bool = True) -> str:
|
64
|
+
"""
|
65
|
+
Capitalize each word and remove symbols.
|
66
|
+
|
67
|
+
Parameters
|
68
|
+
-----------
|
69
|
+
value: :class:`str`
|
70
|
+
The value to convert.
|
71
|
+
strict: :class:`bool`
|
72
|
+
Whether or not to output only alphanumeric characters.
|
73
|
+
|
74
|
+
Returns
|
75
|
+
--------
|
76
|
+
:class:`str`
|
77
|
+
The value in PascalCase.
|
78
|
+
"""
|
79
|
+
|
80
|
+
def substitute_word(symbols, word):
|
81
|
+
if strict:
|
82
|
+
return word.capitalize() # Remove all delimiters
|
83
|
+
|
84
|
+
if word.islower():
|
85
|
+
delimiter_length = len(symbols[:-1]) # Lose one delimiter
|
86
|
+
else:
|
87
|
+
delimiter_length = len(symbols) # Preserve all delimiters
|
88
|
+
|
89
|
+
return ("_" * delimiter_length) + word.capitalize()
|
90
|
+
|
91
|
+
return re.sub(
|
92
|
+
f"({SYMBOLS})({WORD_UPPER}|{WORD})",
|
93
|
+
lambda groups: substitute_word(groups[1], groups[2]),
|
94
|
+
value,
|
95
|
+
)
|
96
|
+
|
97
|
+
|
98
|
+
def camel_case(value: str, strict: bool = True) -> str:
|
99
|
+
"""
|
100
|
+
Capitalize all words except first and remove symbols.
|
101
|
+
|
102
|
+
Parameters
|
103
|
+
-----------
|
104
|
+
value: :class:`str`
|
105
|
+
The value to convert.
|
106
|
+
strict: :class:`bool`
|
107
|
+
Whether or not to output only alphanumeric characters.
|
108
|
+
|
109
|
+
Returns
|
110
|
+
--------
|
111
|
+
:class:`str`
|
112
|
+
The value in camelCase.
|
113
|
+
"""
|
114
|
+
return lowercase_first(pascal_case(value, strict=strict))
|
115
|
+
|
116
|
+
|
117
|
+
def lowercase_first(value: str) -> str:
|
118
|
+
"""
|
119
|
+
Lower cases the first character of the value.
|
120
|
+
|
121
|
+
Parameters
|
122
|
+
----------
|
123
|
+
value: :class:`str`
|
124
|
+
The value to lower case.
|
125
|
+
|
126
|
+
Returns
|
127
|
+
-------
|
128
|
+
:class:`str`
|
129
|
+
The lower cased string.
|
130
|
+
"""
|
131
|
+
return value[0:1].lower() + value[1:]
|
132
|
+
|
133
|
+
|
134
|
+
def sanitize_name(value: str) -> str:
|
135
|
+
# https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles
|
136
|
+
if keyword.iskeyword(value):
|
137
|
+
return f"{value}_"
|
138
|
+
if not value.isidentifier():
|
139
|
+
return f"_{value}"
|
140
|
+
return value
|
File without changes
|
@@ -0,0 +1,180 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import os
|
4
|
+
from typing import (
|
5
|
+
TYPE_CHECKING,
|
6
|
+
)
|
7
|
+
|
8
|
+
from betterproto2_compiler.lib.google import protobuf as google_protobuf
|
9
|
+
from betterproto2_compiler.settings import Settings
|
10
|
+
|
11
|
+
from ..casing import safe_snake_case
|
12
|
+
from .naming import pythonize_class_name
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from ..plugin.models import PluginRequestCompiler
|
16
|
+
|
17
|
+
WRAPPER_TYPES: dict[str, type] = {
|
18
|
+
".google.protobuf.DoubleValue": google_protobuf.DoubleValue,
|
19
|
+
".google.protobuf.FloatValue": google_protobuf.FloatValue,
|
20
|
+
".google.protobuf.Int32Value": google_protobuf.Int32Value,
|
21
|
+
".google.protobuf.Int64Value": google_protobuf.Int64Value,
|
22
|
+
".google.protobuf.UInt32Value": google_protobuf.UInt32Value,
|
23
|
+
".google.protobuf.UInt64Value": google_protobuf.UInt64Value,
|
24
|
+
".google.protobuf.BoolValue": google_protobuf.BoolValue,
|
25
|
+
".google.protobuf.StringValue": google_protobuf.StringValue,
|
26
|
+
".google.protobuf.BytesValue": google_protobuf.BytesValue,
|
27
|
+
}
|
28
|
+
|
29
|
+
|
30
|
+
def parse_source_type_name(field_type_name: str, request: PluginRequestCompiler) -> tuple[str, str]:
|
31
|
+
"""
|
32
|
+
Split full source type name into package and type name.
|
33
|
+
E.g. 'root.package.Message' -> ('root.package', 'Message')
|
34
|
+
'root.Message.SomeEnum' -> ('root', 'Message.SomeEnum')
|
35
|
+
|
36
|
+
The function goes through the symbols that have been defined (names, enums,
|
37
|
+
packages) to find the actual package and name of the object that is referenced.
|
38
|
+
"""
|
39
|
+
if field_type_name[0] != ".":
|
40
|
+
raise RuntimeError("relative names are not supported")
|
41
|
+
field_type_name = field_type_name[1:]
|
42
|
+
parts = field_type_name.split(".")
|
43
|
+
|
44
|
+
answer = None
|
45
|
+
|
46
|
+
# a.b.c:
|
47
|
+
# i=0: "", "a.b.c"
|
48
|
+
# i=1: "a", "b.c"
|
49
|
+
# i=2: "a.b", "c"
|
50
|
+
for i in range(len(parts)):
|
51
|
+
package_name, object_name = ".".join(parts[:i]), ".".join(parts[i:])
|
52
|
+
|
53
|
+
package = request.output_packages.get(package_name)
|
54
|
+
|
55
|
+
if not package:
|
56
|
+
continue
|
57
|
+
|
58
|
+
if object_name in package.messages or object_name in package.enums:
|
59
|
+
if answer:
|
60
|
+
# This should have already been handeled by protoc
|
61
|
+
raise ValueError(f"ambiguous definition: {field_type_name}")
|
62
|
+
answer = package_name, object_name
|
63
|
+
|
64
|
+
if answer:
|
65
|
+
return answer
|
66
|
+
|
67
|
+
raise ValueError(f"can't find type name: {field_type_name}")
|
68
|
+
|
69
|
+
|
70
|
+
def get_type_reference(
|
71
|
+
*,
|
72
|
+
package: str,
|
73
|
+
imports: set,
|
74
|
+
source_type: str,
|
75
|
+
request: PluginRequestCompiler,
|
76
|
+
unwrap: bool = True,
|
77
|
+
settings: Settings,
|
78
|
+
) -> str:
|
79
|
+
"""
|
80
|
+
Return a Python type name for a proto type reference. Adds the import if
|
81
|
+
necessary. Unwraps well known type if required.
|
82
|
+
"""
|
83
|
+
if unwrap:
|
84
|
+
if source_type in WRAPPER_TYPES:
|
85
|
+
wrapped_type = type(WRAPPER_TYPES[source_type]().value)
|
86
|
+
return settings.typing_compiler.optional(wrapped_type.__name__)
|
87
|
+
|
88
|
+
if source_type == ".google.protobuf.Duration":
|
89
|
+
return "datetime.timedelta"
|
90
|
+
|
91
|
+
elif source_type == ".google.protobuf.Timestamp":
|
92
|
+
return "datetime.datetime"
|
93
|
+
|
94
|
+
source_package, source_type = parse_source_type_name(source_type, request)
|
95
|
+
|
96
|
+
current_package: list[str] = package.split(".") if package else []
|
97
|
+
py_package: list[str] = source_package.split(".") if source_package else []
|
98
|
+
py_type: str = pythonize_class_name(source_type)
|
99
|
+
|
100
|
+
if py_package == current_package:
|
101
|
+
return reference_sibling(py_type)
|
102
|
+
|
103
|
+
if py_package[: len(current_package)] == current_package:
|
104
|
+
return reference_descendent(current_package, imports, py_package, py_type)
|
105
|
+
|
106
|
+
if current_package[: len(py_package)] == py_package:
|
107
|
+
return reference_ancestor(current_package, imports, py_package, py_type)
|
108
|
+
|
109
|
+
return reference_cousin(current_package, imports, py_package, py_type)
|
110
|
+
|
111
|
+
|
112
|
+
def reference_absolute(imports: set[str], py_package: list[str], py_type: str) -> str:
|
113
|
+
"""
|
114
|
+
Returns a reference to a python type located in the root, i.e. sys.path.
|
115
|
+
"""
|
116
|
+
string_import = ".".join(py_package)
|
117
|
+
string_alias = safe_snake_case(string_import)
|
118
|
+
imports.add(f"import {string_import} as {string_alias}")
|
119
|
+
return f"{string_alias}.{py_type}"
|
120
|
+
|
121
|
+
|
122
|
+
def reference_sibling(py_type: str) -> str:
|
123
|
+
"""
|
124
|
+
Returns a reference to a python type within the same package as the current package.
|
125
|
+
"""
|
126
|
+
return f"{py_type}"
|
127
|
+
|
128
|
+
|
129
|
+
def reference_descendent(current_package: list[str], imports: set[str], py_package: list[str], py_type: str) -> str:
|
130
|
+
"""
|
131
|
+
Returns a reference to a python type in a package that is a descendent of the
|
132
|
+
current package, and adds the required import that is aliased to avoid name
|
133
|
+
conflicts.
|
134
|
+
"""
|
135
|
+
importing_descendent = py_package[len(current_package) :]
|
136
|
+
string_from = ".".join(importing_descendent[:-1])
|
137
|
+
string_import = importing_descendent[-1]
|
138
|
+
if string_from:
|
139
|
+
string_alias = "_".join(importing_descendent)
|
140
|
+
imports.add(f"from .{string_from} import {string_import} as {string_alias}")
|
141
|
+
return f"{string_alias}.{py_type}"
|
142
|
+
else:
|
143
|
+
imports.add(f"from . import {string_import}")
|
144
|
+
return f"{string_import}.{py_type}"
|
145
|
+
|
146
|
+
|
147
|
+
def reference_ancestor(current_package: list[str], imports: set[str], py_package: list[str], py_type: str) -> str:
|
148
|
+
"""
|
149
|
+
Returns a reference to a python type in a package which is an ancestor to the
|
150
|
+
current package, and adds the required import that is aliased (if possible) to avoid
|
151
|
+
name conflicts.
|
152
|
+
|
153
|
+
Adds trailing __ to avoid name mangling (python.org/dev/peps/pep-0008/#id34).
|
154
|
+
"""
|
155
|
+
distance_up = len(current_package) - len(py_package)
|
156
|
+
if py_package:
|
157
|
+
string_import = py_package[-1]
|
158
|
+
string_alias = f"_{'_' * distance_up}{string_import}__"
|
159
|
+
string_from = f"..{'.' * distance_up}"
|
160
|
+
imports.add(f"from {string_from} import {string_import} as {string_alias}")
|
161
|
+
return f"{string_alias}.{py_type}"
|
162
|
+
else:
|
163
|
+
string_alias = f"{'_' * distance_up}{py_type}__"
|
164
|
+
imports.add(f"from .{'.' * distance_up} import {py_type} as {string_alias}")
|
165
|
+
return string_alias
|
166
|
+
|
167
|
+
|
168
|
+
def reference_cousin(current_package: list[str], imports: set[str], py_package: list[str], py_type: str) -> str:
|
169
|
+
"""
|
170
|
+
Returns a reference to a python type in a package that is not descendent, ancestor
|
171
|
+
or sibling, and adds the required import that is aliased to avoid name conflicts.
|
172
|
+
"""
|
173
|
+
shared_ancestry = os.path.commonprefix([current_package, py_package]) # type: ignore
|
174
|
+
distance_up = len(current_package) - len(shared_ancestry)
|
175
|
+
string_from = f".{'.' * distance_up}" + ".".join(py_package[len(shared_ancestry) : -1])
|
176
|
+
string_import = py_package[-1]
|
177
|
+
# Add trailing __ to avoid name mangling (python.org/dev/peps/pep-0008/#id34)
|
178
|
+
string_alias = f"{'_' * distance_up}" + safe_snake_case(".".join(py_package[len(shared_ancestry) :])) + "__"
|
179
|
+
imports.add(f"from {string_from} import {string_import} as {string_alias}")
|
180
|
+
return f"{string_alias}.{py_type}"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
from betterproto2_compiler import casing
|
2
|
+
|
3
|
+
|
4
|
+
def pythonize_class_name(name: str) -> str:
|
5
|
+
return casing.pascal_case(name)
|
6
|
+
|
7
|
+
|
8
|
+
def pythonize_field_name(name: str) -> str:
|
9
|
+
return casing.safe_snake_case(name)
|
10
|
+
|
11
|
+
|
12
|
+
def pythonize_method_name(name: str) -> str:
|
13
|
+
return casing.safe_snake_case(name)
|
14
|
+
|
15
|
+
|
16
|
+
def pythonize_enum_member_name(name: str, enum_name: str) -> str:
|
17
|
+
enum_name = casing.snake_case(enum_name).upper()
|
18
|
+
find = name.find(enum_name)
|
19
|
+
if find != -1:
|
20
|
+
name = name[find + len(enum_name) :].strip("_")
|
21
|
+
return casing.sanitize_name(name)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
|
3
|
+
from .any import Any
|
4
|
+
from .duration import Duration
|
5
|
+
from .timestamp import Timestamp
|
6
|
+
|
7
|
+
# For each (package, message name), lists the methods that should be added to the message definition.
|
8
|
+
# The source code of the method is read from the `known_types` folder. If imports are needed, they can be directly added
|
9
|
+
# to the template file: they will automatically be removed if not necessary.
|
10
|
+
KNOWN_METHODS: dict[tuple[str, str], list[Callable]] = {
|
11
|
+
("google.protobuf", "Any"): [Any.pack, Any.unpack, Any.to_dict],
|
12
|
+
("google.protobuf", "Timestamp"): [Timestamp.from_datetime, Timestamp.to_datetime, Timestamp.timestamp_to_json],
|
13
|
+
("google.protobuf", "Duration"): [Duration.from_timedelta, Duration.to_timedelta, Duration.delta_to_json],
|
14
|
+
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import betterproto2
|
2
|
+
|
3
|
+
from betterproto2_compiler.lib.google.protobuf import Any as VanillaAny
|
4
|
+
|
5
|
+
default_message_pool = betterproto2.MessagePool() # Only for typing purpose
|
6
|
+
|
7
|
+
|
8
|
+
class Any(VanillaAny):
|
9
|
+
def pack(self, message: betterproto2.Message, message_pool: "betterproto2.MessagePool | None" = None) -> None:
|
10
|
+
"""
|
11
|
+
Pack the given message in the `Any` object.
|
12
|
+
|
13
|
+
The message type must be registered in the message pool, which is done automatically when the module defining
|
14
|
+
the message type is imported.
|
15
|
+
"""
|
16
|
+
message_pool = message_pool or default_message_pool
|
17
|
+
|
18
|
+
self.type_url = message_pool.type_to_url[type(message)]
|
19
|
+
self.value = bytes(message)
|
20
|
+
|
21
|
+
def unpack(self, message_pool: "betterproto2.MessagePool | None" = None) -> betterproto2.Message:
|
22
|
+
"""
|
23
|
+
Return the message packed inside the `Any` object.
|
24
|
+
|
25
|
+
The target message type must be registered in the message pool, which is done automatically when the module
|
26
|
+
defining the message type is imported.
|
27
|
+
"""
|
28
|
+
message_pool = message_pool or default_message_pool
|
29
|
+
|
30
|
+
message_type = message_pool.url_to_type[self.type_url]
|
31
|
+
|
32
|
+
return message_type().parse(self.value)
|
33
|
+
|
34
|
+
def to_dict(self) -> dict: # pyright: ignore [reportIncompatibleMethodOverride]
|
35
|
+
# TOOO improve when dict is updated
|
36
|
+
return {"@type": self.type_url, "value": self.unpack().to_dict()}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import datetime
|
2
|
+
|
3
|
+
from betterproto2_compiler.lib.google.protobuf import Duration as VanillaDuration
|
4
|
+
|
5
|
+
|
6
|
+
class Duration(VanillaDuration):
|
7
|
+
@classmethod
|
8
|
+
def from_timedelta(
|
9
|
+
cls, delta: datetime.timedelta, *, _1_microsecond: datetime.timedelta = datetime.timedelta(microseconds=1)
|
10
|
+
) -> "Duration":
|
11
|
+
total_ms = delta // _1_microsecond
|
12
|
+
seconds = int(total_ms / 1e6)
|
13
|
+
nanos = int((total_ms % 1e6) * 1e3)
|
14
|
+
return cls(seconds, nanos)
|
15
|
+
|
16
|
+
def to_timedelta(self) -> datetime.timedelta:
|
17
|
+
return datetime.timedelta(seconds=self.seconds, microseconds=self.nanos / 1e3)
|
18
|
+
|
19
|
+
@staticmethod
|
20
|
+
def delta_to_json(delta: datetime.timedelta) -> str:
|
21
|
+
parts = str(delta.total_seconds()).split(".")
|
22
|
+
if len(parts) > 1:
|
23
|
+
while len(parts[1]) not in (3, 6, 9):
|
24
|
+
parts[1] = f"{parts[1]}0"
|
25
|
+
return f"{'.'.join(parts)}s"
|