instructure-pysqlsync 0.8.5.dev0__py3-none-any.whl

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.
Files changed (102) hide show
  1. instructure_pysqlsync-0.8.5.dev0.dist-info/METADATA +244 -0
  2. instructure_pysqlsync-0.8.5.dev0.dist-info/RECORD +102 -0
  3. instructure_pysqlsync-0.8.5.dev0.dist-info/WHEEL +5 -0
  4. instructure_pysqlsync-0.8.5.dev0.dist-info/licenses/LICENSE +21 -0
  5. instructure_pysqlsync-0.8.5.dev0.dist-info/top_level.txt +1 -0
  6. instructure_pysqlsync-0.8.5.dev0.dist-info/zip-safe +1 -0
  7. pysqlsync/__init__.py +15 -0
  8. pysqlsync/base.py +1386 -0
  9. pysqlsync/connection.py +101 -0
  10. pysqlsync/data/__init__.py +0 -0
  11. pysqlsync/data/exchange.py +184 -0
  12. pysqlsync/data/generator.py +305 -0
  13. pysqlsync/dialect/README.md +35 -0
  14. pysqlsync/dialect/__init__.py +0 -0
  15. pysqlsync/dialect/delta/__init__.py +0 -0
  16. pysqlsync/dialect/delta/data_types.py +51 -0
  17. pysqlsync/dialect/delta/dependency.py +7 -0
  18. pysqlsync/dialect/delta/engine.py +18 -0
  19. pysqlsync/dialect/delta/generator.py +74 -0
  20. pysqlsync/dialect/delta/object_types.py +87 -0
  21. pysqlsync/dialect/mssql/README.md +23 -0
  22. pysqlsync/dialect/mssql/__init__.py +0 -0
  23. pysqlsync/dialect/mssql/connection.py +146 -0
  24. pysqlsync/dialect/mssql/data_types.py +113 -0
  25. pysqlsync/dialect/mssql/dependency.py +9 -0
  26. pysqlsync/dialect/mssql/discovery.py +99 -0
  27. pysqlsync/dialect/mssql/engine.py +20 -0
  28. pysqlsync/dialect/mssql/generator.py +88 -0
  29. pysqlsync/dialect/mssql/mutation.py +46 -0
  30. pysqlsync/dialect/mssql/object_types.py +135 -0
  31. pysqlsync/dialect/mysql/__init__.py +0 -0
  32. pysqlsync/dialect/mysql/connection.py +126 -0
  33. pysqlsync/dialect/mysql/data_types.py +40 -0
  34. pysqlsync/dialect/mysql/dependency.py +10 -0
  35. pysqlsync/dialect/mysql/discovery.py +145 -0
  36. pysqlsync/dialect/mysql/engine.py +20 -0
  37. pysqlsync/dialect/mysql/generator.py +140 -0
  38. pysqlsync/dialect/mysql/mutation.py +65 -0
  39. pysqlsync/dialect/mysql/object_types.py +75 -0
  40. pysqlsync/dialect/oracle/README.md +22 -0
  41. pysqlsync/dialect/oracle/__init__.py +0 -0
  42. pysqlsync/dialect/oracle/connection.py +141 -0
  43. pysqlsync/dialect/oracle/data_types.py +104 -0
  44. pysqlsync/dialect/oracle/dependency.py +9 -0
  45. pysqlsync/dialect/oracle/discovery.py +231 -0
  46. pysqlsync/dialect/oracle/engine.py +20 -0
  47. pysqlsync/dialect/oracle/generator.py +93 -0
  48. pysqlsync/dialect/oracle/mutation.py +37 -0
  49. pysqlsync/dialect/oracle/object_types.py +59 -0
  50. pysqlsync/dialect/postgresql/__init__.py +0 -0
  51. pysqlsync/dialect/postgresql/connection.py +133 -0
  52. pysqlsync/dialect/postgresql/data_types.py +6 -0
  53. pysqlsync/dialect/postgresql/dependency.py +9 -0
  54. pysqlsync/dialect/postgresql/discovery.py +392 -0
  55. pysqlsync/dialect/postgresql/engine.py +20 -0
  56. pysqlsync/dialect/postgresql/generator.py +99 -0
  57. pysqlsync/dialect/postgresql/mutation.py +82 -0
  58. pysqlsync/dialect/postgresql/object_types.py +99 -0
  59. pysqlsync/dialect/redshift/__init__.py +0 -0
  60. pysqlsync/dialect/redshift/connection.py +156 -0
  61. pysqlsync/dialect/redshift/data_types.py +10 -0
  62. pysqlsync/dialect/redshift/dependency.py +9 -0
  63. pysqlsync/dialect/redshift/engine.py +20 -0
  64. pysqlsync/dialect/redshift/generator.py +82 -0
  65. pysqlsync/dialect/snowflake/__init__.py +0 -0
  66. pysqlsync/dialect/snowflake/data_types.py +18 -0
  67. pysqlsync/dialect/snowflake/dependency.py +9 -0
  68. pysqlsync/dialect/snowflake/engine.py +18 -0
  69. pysqlsync/dialect/snowflake/generator.py +64 -0
  70. pysqlsync/dialect/snowflake/object_types.py +84 -0
  71. pysqlsync/dialect/test/__init__.py +0 -0
  72. pysqlsync/dialect/test/dependency.py +10 -0
  73. pysqlsync/dialect/trino/__init__.py +0 -0
  74. pysqlsync/dialect/trino/connection.py +86 -0
  75. pysqlsync/dialect/trino/dependency.py +9 -0
  76. pysqlsync/dialect/trino/discovery.py +21 -0
  77. pysqlsync/dialect/trino/engine.py +20 -0
  78. pysqlsync/dialect/trino/generator.py +31 -0
  79. pysqlsync/factory.py +169 -0
  80. pysqlsync/formation/__init__.py +0 -0
  81. pysqlsync/formation/constraints.py +78 -0
  82. pysqlsync/formation/data_types.py +192 -0
  83. pysqlsync/formation/discovery.py +390 -0
  84. pysqlsync/formation/inspection.py +179 -0
  85. pysqlsync/formation/mutation.py +320 -0
  86. pysqlsync/formation/object_dict.py +79 -0
  87. pysqlsync/formation/object_types.py +872 -0
  88. pysqlsync/formation/py_to_sql.py +1134 -0
  89. pysqlsync/formation/sql_to_py.py +194 -0
  90. pysqlsync/model/__init__.py +0 -0
  91. pysqlsync/model/data_types.py +397 -0
  92. pysqlsync/model/entity_types.py +68 -0
  93. pysqlsync/model/id_types.py +193 -0
  94. pysqlsync/model/key_types.py +38 -0
  95. pysqlsync/model/properties.py +234 -0
  96. pysqlsync/py.typed +0 -0
  97. pysqlsync/python_types.py +192 -0
  98. pysqlsync/resultset.py +106 -0
  99. pysqlsync/util/__init__.py +0 -0
  100. pysqlsync/util/dataclasses.py +80 -0
  101. pysqlsync/util/dispatch.py +28 -0
  102. pysqlsync/util/typing.py +13 -0
@@ -0,0 +1,193 @@
1
+ import abc
2
+ from dataclasses import dataclass
3
+ from typing import Optional, Protocol, runtime_checkable
4
+
5
+ ID_QUOTE_CHAR = '"'
6
+
7
+
8
+ def quote_id(name: str) -> str:
9
+ return name.replace(ID_QUOTE_CHAR, 2 * ID_QUOTE_CHAR)
10
+
11
+
12
+ @runtime_checkable
13
+ class SupportsQuotedId(Protocol):
14
+ __slots__ = ()
15
+
16
+ @property
17
+ @abc.abstractmethod
18
+ def quoted_id(self) -> str:
19
+ "A fully-quoted identifier."
20
+ ...
21
+
22
+
23
+ @runtime_checkable
24
+ class SupportsLocalId(Protocol):
25
+ __slots__ = ()
26
+
27
+ @property
28
+ @abc.abstractmethod
29
+ def local_id(self) -> str:
30
+ "Unquoted component of an identifier to be used in a local context, e.g. columns of a table."
31
+ ...
32
+
33
+ @property
34
+ @abc.abstractmethod
35
+ def quoted_id(self) -> str:
36
+ "A fully-quoted identifier."
37
+ ...
38
+
39
+
40
+ @runtime_checkable
41
+ class SupportsQualifiedId(Protocol):
42
+ __slots__ = ()
43
+
44
+ @property
45
+ @abc.abstractmethod
46
+ def scope_id(self) -> Optional[str]:
47
+ "Unquoted scope identifier."
48
+ ...
49
+
50
+ @property
51
+ @abc.abstractmethod
52
+ def local_id(self) -> str:
53
+ "Unquoted component of an identifier to be used in a local context, e.g. columns of a table."
54
+ ...
55
+
56
+ @property
57
+ @abc.abstractmethod
58
+ def compact_id(self) -> str:
59
+ "An unquoted composite identifier."
60
+ ...
61
+
62
+ @property
63
+ @abc.abstractmethod
64
+ def quoted_id(self) -> str:
65
+ "A fully-quoted identifier."
66
+ ...
67
+
68
+ def rename(self, id: str) -> "SupportsQualifiedId": ...
69
+
70
+
71
+ @runtime_checkable
72
+ class SupportsName(Protocol):
73
+ __slots__ = ()
74
+
75
+ @property
76
+ @abc.abstractmethod
77
+ def name(self) -> SupportsLocalId: ...
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class LocalId:
82
+ id: str
83
+
84
+ @property
85
+ def local_id(self) -> str:
86
+ "Unquoted identifier."
87
+
88
+ return self.id
89
+
90
+ @property
91
+ def quoted_id(self) -> str:
92
+ return ID_QUOTE_CHAR + quote_id(self.id) + ID_QUOTE_CHAR
93
+
94
+ def __str__(self) -> str:
95
+ "Quotes an identifier to be embedded in a SQL statement."
96
+
97
+ return self.quoted_id
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class PrefixedId:
102
+ namespace: Optional[str]
103
+ id: str
104
+
105
+ @property
106
+ def scope_id(self) -> Optional[str]:
107
+ return None
108
+
109
+ @property
110
+ def local_id(self) -> str:
111
+ return f"{self.namespace}__{self.id}" if self.namespace is not None else self.id
112
+
113
+ @property
114
+ def compact_id(self) -> str:
115
+ return self.local_id
116
+
117
+ @property
118
+ def quoted_id(self) -> str:
119
+ if self.namespace is not None:
120
+ return ID_QUOTE_CHAR + quote_id(self.namespace) + "__" + quote_id(self.id) + ID_QUOTE_CHAR
121
+ else:
122
+ return ID_QUOTE_CHAR + quote_id(self.id) + ID_QUOTE_CHAR
123
+
124
+ def rename(self, id: str) -> "SupportsQualifiedId":
125
+ return PrefixedId(self.namespace, id)
126
+
127
+ def __str__(self) -> str:
128
+ "Quotes a qualified identifier to be embedded in a SQL statement."
129
+
130
+ return self.quoted_id
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class QualifiedId:
135
+ namespace: Optional[str]
136
+ id: str
137
+
138
+ @property
139
+ def scope_id(self) -> Optional[str]:
140
+ return self.namespace
141
+
142
+ @property
143
+ def local_id(self) -> str:
144
+ return self.id
145
+
146
+ @property
147
+ def compact_id(self) -> str:
148
+ if self.namespace is not None:
149
+ return f"{self.namespace}.{self.id}"
150
+ else:
151
+ return self.id
152
+
153
+ @property
154
+ def quoted_id(self) -> str:
155
+ if self.namespace is not None:
156
+ return ID_QUOTE_CHAR + quote_id(self.namespace) + ID_QUOTE_CHAR + "." + ID_QUOTE_CHAR + quote_id(self.id) + ID_QUOTE_CHAR
157
+ else:
158
+ return ID_QUOTE_CHAR + quote_id(self.id) + ID_QUOTE_CHAR
159
+
160
+ def rename(self, id: str) -> "SupportsQualifiedId":
161
+ return QualifiedId(self.namespace, id)
162
+
163
+ def __str__(self) -> str:
164
+ "Quotes a qualified identifier to be embedded in a SQL statement."
165
+
166
+ return self.quoted_id
167
+
168
+
169
+ @dataclass(frozen=True)
170
+ class GlobalId:
171
+ id: str
172
+
173
+ @property
174
+ def scope_id(self) -> Optional[str]:
175
+ return None
176
+
177
+ @property
178
+ def local_id(self) -> str:
179
+ return self.id
180
+
181
+ @property
182
+ def compact_id(self) -> str:
183
+ return self.id
184
+
185
+ @property
186
+ def quoted_id(self) -> str:
187
+ return self.id
188
+
189
+ def rename(self, id: str) -> "SupportsQualifiedId":
190
+ return GlobalId(id)
191
+
192
+ def __str__(self) -> str:
193
+ return self.id
@@ -0,0 +1,38 @@
1
+ from typing import Annotated, TypeVar, Union
2
+
3
+ T = TypeVar("T")
4
+
5
+
6
+ class PrimaryKeyTag:
7
+ "Marks a field as the primary key of a table."
8
+
9
+ def __repr__(self) -> str:
10
+ return "PrimaryKey"
11
+
12
+
13
+ class IdentityTag:
14
+ "Marks a field as an identity column in a table."
15
+
16
+ def __repr__(self) -> str:
17
+ return "Identity"
18
+
19
+
20
+ class UniqueTag:
21
+ "Marks a field as a column with a unique constraint."
22
+
23
+ def __repr__(self) -> str:
24
+ return "Unique"
25
+
26
+
27
+ class DefaultTag:
28
+ "The placeholder value DEFAULT."
29
+
30
+ def __repr__(self) -> str:
31
+ return "DEFAULT"
32
+
33
+
34
+ DEFAULT = DefaultTag()
35
+
36
+ PrimaryKey = Annotated[T, PrimaryKeyTag()]
37
+ Identity = Annotated[Union[T, DefaultTag], IdentityTag()]
38
+ Unique = Annotated[T, UniqueTag()]
@@ -0,0 +1,234 @@
1
+ import dataclasses
2
+ import datetime
3
+ import decimal
4
+ import ipaddress
5
+ import typing
6
+ import uuid
7
+ from dataclasses import dataclass
8
+ from typing import Annotated, Any, Union
9
+
10
+ from strong_typing.inspection import (
11
+ DataclassInstance,
12
+ TypeLike,
13
+ dataclass_fields,
14
+ get_annotation,
15
+ is_dataclass_type,
16
+ is_type_enum,
17
+ is_type_literal,
18
+ is_type_optional,
19
+ is_type_union,
20
+ unwrap_annotated_type,
21
+ unwrap_literal_types,
22
+ unwrap_optional_type,
23
+ unwrap_union_types,
24
+ )
25
+
26
+ from .key_types import DefaultTag, IdentityTag, PrimaryKeyTag, UniqueTag
27
+
28
+
29
+ def is_primary_key_type(field_type: TypeLike) -> bool:
30
+ "Checks if the field type is marked as the primary key of a table."
31
+
32
+ return get_annotation(field_type, PrimaryKeyTag) is not None
33
+
34
+
35
+ def is_identity_type(field_type: TypeLike) -> bool:
36
+ "Checks if the field type is marked as an identity column in a table."
37
+
38
+ return get_annotation(field_type, IdentityTag) is not None
39
+
40
+
41
+ def is_unique_type(field_type: TypeLike) -> bool:
42
+ "Checks if the field type is marked as a unique column in a table."
43
+
44
+ return get_annotation(field_type, UniqueTag) is not None
45
+
46
+
47
+ def get_primary_key_name(class_type: type[DataclassInstance]) -> str:
48
+ "Fetches the primary key of the table."
49
+
50
+ for field in dataclass_fields(class_type):
51
+ if is_primary_key_type(field.type):
52
+ return field.name
53
+
54
+ raise TypeError(f"table type has no primary key: {class_type.__name__}")
55
+
56
+
57
+ def get_primary_key_name_type(class_type: type[DataclassInstance]) -> tuple[str, type]:
58
+ "Returns the primary key of the table and its plain representation type."
59
+
60
+ for field in dataclass_fields(class_type):
61
+ props = get_field_properties(field.type)
62
+ if props.is_primary:
63
+ return field.name, props.tsv_type
64
+
65
+ raise TypeError(f"table type has no primary key: {class_type.__name__}")
66
+
67
+
68
+ def is_constraint(item: Any) -> bool:
69
+ return isinstance(item, (PrimaryKeyTag, IdentityTag, UniqueTag))
70
+
71
+
72
+ def tsv_type(plain_type: TypeLike) -> type:
73
+ "Python type that represents the TSV storage type for this data."
74
+
75
+ plain_type = unwrap_annotated_type(plain_type)
76
+
77
+ if is_type_enum(plain_type):
78
+ return str
79
+ elif is_type_literal(plain_type):
80
+ literal_types = set(tsv_type(t) for t in unwrap_literal_types(plain_type))
81
+ if len(literal_types) != 1:
82
+ raise TypeError(f"inconsistent literal value types: {literal_types}")
83
+ return literal_types.pop()
84
+ elif is_type_union(plain_type):
85
+ union_types = set(tsv_type(t) for t in unwrap_union_types(plain_type))
86
+ if len(union_types) != 1:
87
+ if len(union_types) == 2 and ipaddress.IPv4Address in union_types and ipaddress.IPv6Address in union_types:
88
+ return typing.cast(type, plain_type)
89
+ raise TypeError(f"inconsistent union types: {union_types}")
90
+ return union_types.pop()
91
+ elif is_dataclass_type(plain_type):
92
+ return dict
93
+ elif plain_type is bool:
94
+ return bool
95
+ elif plain_type is bytes:
96
+ return bytes
97
+ elif plain_type is int:
98
+ return int
99
+ elif plain_type is float:
100
+ return float
101
+ elif plain_type is str:
102
+ return str
103
+ elif plain_type is uuid.UUID:
104
+ return uuid.UUID
105
+ elif plain_type is datetime.datetime:
106
+ return datetime.datetime
107
+ elif plain_type is datetime.date:
108
+ return datetime.date
109
+ elif plain_type is datetime.time:
110
+ return datetime.time
111
+ elif plain_type is decimal.Decimal:
112
+ return decimal.Decimal
113
+ elif plain_type is ipaddress.IPv4Address:
114
+ return ipaddress.IPv4Address
115
+ elif plain_type is ipaddress.IPv6Address:
116
+ return ipaddress.IPv6Address
117
+
118
+ origin_type = typing.get_origin(plain_type)
119
+ if origin_type is list: # JSON
120
+ return list
121
+ elif origin_type is set: # JSON
122
+ return set
123
+ elif origin_type is dict: # JSON
124
+ return dict
125
+
126
+ raise TypeError(f"unsupported TSV value type: {plain_type}")
127
+
128
+
129
+ @dataclass
130
+ class FieldProperties:
131
+ """
132
+ Captures type information associated with a field type.
133
+
134
+ :param plain_type: Unadorned type without any metadata.
135
+ :param nullable: True if the field is optional.
136
+ :param metadata: Any metadata that is not a constraint such as identity, primary key or unique.
137
+ :param is_primary: True if the field is a primary key.
138
+ :param is_identity: True if the field is generated as identity.
139
+ :param is_unique: True if values of this type must be unique.
140
+ """
141
+
142
+ plain_type: TypeLike
143
+ nullable: bool
144
+ metadata: tuple[Any, ...]
145
+ is_primary: bool
146
+ is_identity: bool
147
+ is_unique: bool
148
+
149
+ @property
150
+ def field_type(self) -> TypeLike:
151
+ "Type without constraint annotations such as identity, primary key, or unique."
152
+
153
+ if self.metadata:
154
+ # type becomes Annotated[T, ...]
155
+ return Annotated[(self.plain_type, *self.metadata)]
156
+ else:
157
+ # type becomes a regular type
158
+ return self.plain_type
159
+
160
+ @property
161
+ def tsv_type(self) -> type:
162
+ "Python type that represents the TSV storage type for this data."
163
+
164
+ return tsv_type(self.plain_type)
165
+
166
+
167
+ def _get_field_metadata(field_type: TypeLike) -> list[Any]:
168
+ # field has a type of Annotated[T, ...]
169
+ return list(getattr(field_type, "__metadata__", ()))
170
+
171
+
172
+ def get_field_properties(field_type: TypeLike) -> FieldProperties:
173
+ "Extracts column properties such as primary key, identity or unique constraints."
174
+
175
+ metadata = _get_field_metadata(field_type)
176
+ plain_type = unwrap_annotated_type(field_type)
177
+
178
+ # check if field has DEFAULT as a union type member
179
+ if is_type_union(plain_type):
180
+ union_types = list(unwrap_union_types(plain_type))
181
+ if DefaultTag in union_types:
182
+ union_types.remove(DefaultTag)
183
+ plain_type = Union[tuple(union_types)] # may cease to become a union type
184
+
185
+ # check if field is nullable
186
+ if is_type_optional(plain_type):
187
+ nullable = True
188
+ plain_type = unwrap_optional_type(plain_type)
189
+ else:
190
+ nullable = False
191
+
192
+ metadata.extend(_get_field_metadata(plain_type))
193
+ plain_type = unwrap_annotated_type(plain_type)
194
+
195
+ if not metadata:
196
+ # field has a type without annotations or constraints
197
+ return FieldProperties(
198
+ plain_type=plain_type,
199
+ nullable=nullable,
200
+ metadata=(),
201
+ is_primary=False,
202
+ is_identity=False,
203
+ is_unique=False,
204
+ )
205
+
206
+ # check for constraints
207
+ is_primary = any(isinstance(m, PrimaryKeyTag) for m in metadata)
208
+ is_identity = any(isinstance(m, IdentityTag) for m in metadata)
209
+ is_unique = any(isinstance(m, UniqueTag) for m in metadata)
210
+
211
+ # filter annotations that represent constraints
212
+ metadata = [item for item in metadata if not is_constraint(item)]
213
+
214
+ return FieldProperties(
215
+ plain_type=plain_type,
216
+ nullable=nullable,
217
+ metadata=tuple(metadata),
218
+ is_primary=is_primary,
219
+ is_identity=is_identity,
220
+ is_unique=is_unique,
221
+ )
222
+
223
+
224
+ @dataclass
225
+ class ClassProperties:
226
+ fields: tuple[FieldProperties, ...]
227
+
228
+ @property
229
+ def tsv_types(self) -> tuple[type, ...]:
230
+ return tuple(prop.tsv_type for prop in self.fields)
231
+
232
+
233
+ def get_class_properties(class_type: type[DataclassInstance]) -> ClassProperties:
234
+ return ClassProperties(tuple(get_field_properties(field.type) for field in dataclasses.fields(class_type)))
pysqlsync/py.typed ADDED
File without changes
@@ -0,0 +1,192 @@
1
+ import dataclasses
2
+ import enum
3
+ import inspect
4
+ import io
5
+ import re
6
+ import sys
7
+ import textwrap
8
+ import typing
9
+ from types import ModuleType
10
+ from typing import TextIO
11
+
12
+ from strong_typing.docstring import has_docstring, parse_type
13
+ from strong_typing.inspection import (
14
+ DataclassInstance,
15
+ get_module_classes,
16
+ is_dataclass_type,
17
+ is_type_enum,
18
+ )
19
+ from strong_typing.name import TypeFormatter
20
+
21
+
22
+ def module_to_stream(module: ModuleType, target: TextIO) -> None:
23
+ "Generates Python code of all declared types in a module."
24
+
25
+ print("from __future__ import annotations", file=target)
26
+ print("import dataclasses", file=target)
27
+ print("import enum", file=target)
28
+ print("from datetime import datetime, date, time", file=target)
29
+ print("from decimal import Decimal", file=target)
30
+ print("from uuid import UUID", file=target)
31
+ print(
32
+ "from typing import Annotated, Dict, List, Literal, Optional, Union",
33
+ file=target,
34
+ )
35
+ print(
36
+ "from strong_typing.auxiliary import int16, int32, int64, float32, float64, MaxLength, Precision, TimePrecision",
37
+ file=target,
38
+ )
39
+ print(file=target)
40
+
41
+ for cls in get_module_classes(module):
42
+ if is_type_enum(cls):
43
+ enum_class_to_stream(cls, target)
44
+ elif is_dataclass_type(cls):
45
+ dataclass_to_stream(cls, target)
46
+ else:
47
+ raise NotImplementedError("classes in module must be of enumeration or data-class type")
48
+ print(file=target)
49
+
50
+
51
+ def module_to_code(module: ModuleType) -> str:
52
+ "Generates Python code of all declared types in a module."
53
+
54
+ with io.StringIO() as out:
55
+ module_to_stream(module, out)
56
+ return out.getvalue()
57
+
58
+
59
+ def dataclass_to_stream(typ: type[DataclassInstance], target: TextIO) -> None:
60
+ "Generates Python code corresponding to a dataclass type."
61
+
62
+ print("@dataclasses.dataclass", file=target)
63
+ print(f"class {typ.__name__}:", file=target)
64
+
65
+ # check if class has a doc-string other than the auto-generated string assigned by @dataclass
66
+ if has_docstring(typ):
67
+ if "\n" in typing.cast(str, typ.__doc__):
68
+ ds = parse_type(typ)
69
+ print(' """', file=target)
70
+
71
+ if ds.short_description:
72
+ _wrap_print(ds.short_description, file=target)
73
+ if ds.long_description:
74
+ print(file=target)
75
+ _wrap_print(ds.long_description, file=target)
76
+
77
+ if ds.short_description and (ds.params or ds.returns):
78
+ print(file=target)
79
+
80
+ for name, param in ds.params.items():
81
+ _wrap_print(f":param {name}: {param.description}", file=target)
82
+ if ds.returns:
83
+ _wrap_print(f":returns: {ds.returns.description}", file=target)
84
+
85
+ print(' """', file=target)
86
+ else:
87
+ print(f" {repr(typ.__doc__)}", file=target)
88
+ print(file=target)
89
+
90
+ # class variables (e.g. "primary_key")
91
+ field_names = [field.name for field in dataclasses.fields(typ)]
92
+ variables = {
93
+ name: value
94
+ for name, value in inspect.getmembers(typ, lambda m: not inspect.isroutine(m))
95
+ if not re.match(r"^__.+__$", name) and name not in field_names
96
+ }
97
+ if variables:
98
+ for name, value in variables.items():
99
+ print(f" {name} = {repr(value)}", file=target)
100
+ print(file=target)
101
+
102
+ # member variables
103
+ fmt = TypeFormatter(context=sys.modules[typ.__module__])
104
+ for field in dataclasses.fields(typ):
105
+ type_name = fmt.python_type_to_str(field.type)
106
+ metadata = dict(field.metadata)
107
+
108
+ field_initializer: dict[str, str] = {}
109
+ if field.default is not dataclasses.MISSING:
110
+ field_initializer["default"] = repr(field.default)
111
+ if field.default_factory is not dataclasses.MISSING:
112
+ field_initializer["default_factory"] = fmt.python_type_to_str(field.default_factory)
113
+ if metadata:
114
+ field_initializer["metadata"] = repr(metadata)
115
+
116
+ if not field_initializer:
117
+ initializer = ""
118
+ elif field.default is not dataclasses.MISSING and len(field_initializer) == 1:
119
+ initializer = f" = {repr(field.default)}"
120
+ else:
121
+ initializer_list = ", ".join(f"{key} = {value}" for key, value in field_initializer.items())
122
+ initializer = f" = field({initializer_list})"
123
+
124
+ print(f" {field.name}: {type_name}{initializer}", file=target)
125
+
126
+
127
+ def dataclasses_to_stream(types: list[type[DataclassInstance]], target: TextIO) -> None:
128
+ "Generates Python code corresponding to a set of dataclass types."
129
+
130
+ for typ in types:
131
+ dataclass_to_stream(typ, target)
132
+
133
+
134
+ def dataclass_to_code(data_type: type[DataclassInstance]) -> str:
135
+ "Generates Python code corresponding to a dataclass type."
136
+
137
+ return dataclasses_to_code([data_type])
138
+
139
+
140
+ def dataclasses_to_code(types: list[type[DataclassInstance]]) -> str:
141
+ "Generates Python code corresponding to a set of dataclass types."
142
+
143
+ f = io.StringIO()
144
+ dataclasses_to_stream(types, f)
145
+ return f.getvalue()
146
+
147
+
148
+ def enum_class_to_stream(enum_class: type[enum.Enum], target: TextIO) -> None:
149
+ "Writes an enumeration class as a class definition."
150
+
151
+ print("@enum.unique", file=target)
152
+ print(f"class {enum_class.__name__}(enum.Enum):", file=target)
153
+ if enum_class.__doc__:
154
+ print(f" {repr(enum_class.__doc__)}", file=target)
155
+ print(file=target)
156
+
157
+ for e in enum_class:
158
+ value = repr(e.value)
159
+ print(f" {e.name} = {value}", file=target)
160
+
161
+ for e in enum_class:
162
+ if not e.__doc__ or e.__doc__ == enum_class.__doc__:
163
+ continue
164
+
165
+ print(
166
+ f"{enum_class.__name__}.{e.name}.__doc__ = {repr(e.__doc__)}",
167
+ file=target,
168
+ )
169
+
170
+
171
+ def enum_class_to_code(enum_class: type[enum.Enum]) -> str:
172
+ "Returns an enumeration class as a class definition string."
173
+
174
+ with io.StringIO() as out:
175
+ enum_class_to_stream(enum_class, out)
176
+ return out.getvalue()
177
+
178
+
179
+ def _wrap_print(text: str, file: TextIO) -> None:
180
+ if not text:
181
+ return
182
+
183
+ # wrap long lines
184
+ for line in textwrap.wrap(
185
+ text,
186
+ width=139,
187
+ initial_indent=" ",
188
+ subsequent_indent=" ",
189
+ break_long_words=False,
190
+ break_on_hyphens=False,
191
+ ):
192
+ print(line, file=file)