google-cloud-bigtable 2.30.0__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 (115) hide show
  1. google/cloud/bigtable/__init__.py +25 -0
  2. google/cloud/bigtable/app_profile.py +377 -0
  3. google/cloud/bigtable/backup.py +490 -0
  4. google/cloud/bigtable/batcher.py +414 -0
  5. google/cloud/bigtable/client.py +475 -0
  6. google/cloud/bigtable/cluster.py +541 -0
  7. google/cloud/bigtable/column_family.py +362 -0
  8. google/cloud/bigtable/data/__init__.py +103 -0
  9. google/cloud/bigtable/data/_async/__init__.py +25 -0
  10. google/cloud/bigtable/data/_async/_mutate_rows.py +226 -0
  11. google/cloud/bigtable/data/_async/_read_rows.py +363 -0
  12. google/cloud/bigtable/data/_async/client.py +1522 -0
  13. google/cloud/bigtable/data/_async/mutations_batcher.py +534 -0
  14. google/cloud/bigtable/data/_cross_sync/__init__.py +20 -0
  15. google/cloud/bigtable/data/_cross_sync/_decorators.py +441 -0
  16. google/cloud/bigtable/data/_cross_sync/_mapping_meta.py +64 -0
  17. google/cloud/bigtable/data/_cross_sync/cross_sync.py +334 -0
  18. google/cloud/bigtable/data/_helpers.py +249 -0
  19. google/cloud/bigtable/data/_sync_autogen/_mutate_rows.py +182 -0
  20. google/cloud/bigtable/data/_sync_autogen/_read_rows.py +304 -0
  21. google/cloud/bigtable/data/_sync_autogen/client.py +1293 -0
  22. google/cloud/bigtable/data/_sync_autogen/mutations_batcher.py +449 -0
  23. google/cloud/bigtable/data/exceptions.py +340 -0
  24. google/cloud/bigtable/data/execute_query/__init__.py +43 -0
  25. google/cloud/bigtable/data/execute_query/_async/__init__.py +13 -0
  26. google/cloud/bigtable/data/execute_query/_async/execute_query_iterator.py +291 -0
  27. google/cloud/bigtable/data/execute_query/_byte_cursor.py +123 -0
  28. google/cloud/bigtable/data/execute_query/_checksum.py +43 -0
  29. google/cloud/bigtable/data/execute_query/_parameters_formatting.py +155 -0
  30. google/cloud/bigtable/data/execute_query/_query_result_parsing_utils.py +134 -0
  31. google/cloud/bigtable/data/execute_query/_reader.py +122 -0
  32. google/cloud/bigtable/data/execute_query/_sync_autogen/execute_query_iterator.py +239 -0
  33. google/cloud/bigtable/data/execute_query/metadata.py +399 -0
  34. google/cloud/bigtable/data/execute_query/values.py +123 -0
  35. google/cloud/bigtable/data/mutations.py +380 -0
  36. google/cloud/bigtable/data/read_modify_write_rules.py +112 -0
  37. google/cloud/bigtable/data/read_rows_query.py +536 -0
  38. google/cloud/bigtable/data/row.py +535 -0
  39. google/cloud/bigtable/data/row_filters.py +968 -0
  40. google/cloud/bigtable/encryption_info.py +64 -0
  41. google/cloud/bigtable/enums.py +223 -0
  42. google/cloud/bigtable/error.py +64 -0
  43. google/cloud/bigtable/gapic_version.py +16 -0
  44. google/cloud/bigtable/helpers.py +31 -0
  45. google/cloud/bigtable/instance.py +789 -0
  46. google/cloud/bigtable/policy.py +255 -0
  47. google/cloud/bigtable/row.py +1267 -0
  48. google/cloud/bigtable/row_data.py +380 -0
  49. google/cloud/bigtable/row_filters.py +838 -0
  50. google/cloud/bigtable/row_merger.py +250 -0
  51. google/cloud/bigtable/row_set.py +213 -0
  52. google/cloud/bigtable/table.py +1409 -0
  53. google/cloud/bigtable_admin/__init__.py +410 -0
  54. google/cloud/bigtable_admin/gapic_version.py +16 -0
  55. google/cloud/bigtable_admin/py.typed +2 -0
  56. google/cloud/bigtable_admin_v2/__init__.py +250 -0
  57. google/cloud/bigtable_admin_v2/gapic_metadata.json +962 -0
  58. google/cloud/bigtable_admin_v2/gapic_version.py +16 -0
  59. google/cloud/bigtable_admin_v2/py.typed +2 -0
  60. google/cloud/bigtable_admin_v2/services/__init__.py +15 -0
  61. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/__init__.py +22 -0
  62. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/async_client.py +3465 -0
  63. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/client.py +3944 -0
  64. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/pagers.py +681 -0
  65. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/__init__.py +40 -0
  66. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/base.py +751 -0
  67. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc.py +1249 -0
  68. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc_asyncio.py +1579 -0
  69. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/rest.py +6820 -0
  70. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/rest_base.py +1746 -0
  71. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/__init__.py +22 -0
  72. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/async_client.py +3472 -0
  73. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/client.py +3949 -0
  74. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/pagers.py +669 -0
  75. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/__init__.py +40 -0
  76. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/base.py +708 -0
  77. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc.py +1257 -0
  78. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc_asyncio.py +1549 -0
  79. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/rest.py +6609 -0
  80. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/rest_base.py +1714 -0
  81. google/cloud/bigtable_admin_v2/types/__init__.py +248 -0
  82. google/cloud/bigtable_admin_v2/types/bigtable_instance_admin.py +1364 -0
  83. google/cloud/bigtable_admin_v2/types/bigtable_table_admin.py +1715 -0
  84. google/cloud/bigtable_admin_v2/types/common.py +81 -0
  85. google/cloud/bigtable_admin_v2/types/instance.py +819 -0
  86. google/cloud/bigtable_admin_v2/types/table.py +1028 -0
  87. google/cloud/bigtable_admin_v2/types/types.py +776 -0
  88. google/cloud/bigtable_v2/__init__.py +136 -0
  89. google/cloud/bigtable_v2/gapic_metadata.json +193 -0
  90. google/cloud/bigtable_v2/gapic_version.py +16 -0
  91. google/cloud/bigtable_v2/py.typed +2 -0
  92. google/cloud/bigtable_v2/services/__init__.py +15 -0
  93. google/cloud/bigtable_v2/services/bigtable/__init__.py +22 -0
  94. google/cloud/bigtable_v2/services/bigtable/async_client.py +1741 -0
  95. google/cloud/bigtable_v2/services/bigtable/client.py +2194 -0
  96. google/cloud/bigtable_v2/services/bigtable/transports/__init__.py +38 -0
  97. google/cloud/bigtable_v2/services/bigtable/transports/base.py +343 -0
  98. google/cloud/bigtable_v2/services/bigtable/transports/grpc.py +660 -0
  99. google/cloud/bigtable_v2/services/bigtable/transports/grpc_asyncio.py +762 -0
  100. google/cloud/bigtable_v2/services/bigtable/transports/rest.py +2489 -0
  101. google/cloud/bigtable_v2/services/bigtable/transports/rest_base.py +711 -0
  102. google/cloud/bigtable_v2/types/__init__.py +138 -0
  103. google/cloud/bigtable_v2/types/bigtable.py +1531 -0
  104. google/cloud/bigtable_v2/types/data.py +1612 -0
  105. google/cloud/bigtable_v2/types/feature_flags.py +119 -0
  106. google/cloud/bigtable_v2/types/request_stats.py +171 -0
  107. google/cloud/bigtable_v2/types/response_params.py +64 -0
  108. google/cloud/bigtable_v2/types/types.py +579 -0
  109. google_cloud_bigtable-2.30.0.data/scripts/fixup_bigtable_admin_v2_keywords.py +233 -0
  110. google_cloud_bigtable-2.30.0.data/scripts/fixup_bigtable_v2_keywords.py +186 -0
  111. google_cloud_bigtable-2.30.0.dist-info/LICENSE +202 -0
  112. google_cloud_bigtable-2.30.0.dist-info/METADATA +154 -0
  113. google_cloud_bigtable-2.30.0.dist-info/RECORD +115 -0
  114. google_cloud_bigtable-2.30.0.dist-info/WHEEL +5 -0
  115. google_cloud_bigtable-2.30.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,441 @@
1
+ # Copyright 2024 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """
15
+ Contains a set of AstDecorator classes, which define the behavior of CrossSync decorators.
16
+ Each AstDecorator class is used through @CrossSync.<decorator_name>
17
+ """
18
+ from __future__ import annotations
19
+ from typing import TYPE_CHECKING, Iterable
20
+
21
+ if TYPE_CHECKING:
22
+ import ast
23
+ from typing import Callable, Any
24
+
25
+
26
+ class AstDecorator:
27
+ """
28
+ Helper class for CrossSync decorators used for guiding ast transformations.
29
+
30
+ AstDecorators are accessed in two ways:
31
+ 1. The decorations are used directly as method decorations in the async client,
32
+ wrapping existing classes and methods
33
+ 2. The decorations are read back when processing the AST transformations when
34
+ generating sync code.
35
+
36
+ This class allows the same decorator to be used in both contexts.
37
+
38
+ Typically, AstDecorators act as a no-op in async code, and the arguments simply
39
+ provide configuration guidance for the sync code generation.
40
+ """
41
+
42
+ @classmethod
43
+ def decorator(cls, *args, **kwargs) -> Callable[..., Any]:
44
+ """
45
+ Provides a callable that can be used as a decorator function in async code
46
+
47
+ AstDecorator.decorate is called by CrossSync when attaching decorators to
48
+ the CrossSync class.
49
+
50
+ This method creates a new instance of the class, using the arguments provided
51
+ to the decorator, and defers to the async_decorator method of the instance
52
+ to build the wrapper function.
53
+
54
+ Arguments:
55
+ *args: arguments to the decorator
56
+ **kwargs: keyword arguments to the decorator
57
+ """
58
+ # decorators with no arguments will provide the function to be wrapped
59
+ # as the first argument. Pull it out if it exists
60
+ func = None
61
+ if len(args) == 1 and callable(args[0]):
62
+ func = args[0]
63
+ args = args[1:]
64
+ # create new AstDecorator instance from given decorator arguments
65
+ new_instance = cls(*args, **kwargs)
66
+ # build wrapper
67
+ wrapper = new_instance.async_decorator()
68
+ if wrapper is None:
69
+ # if no wrapper, return no-op decorator
70
+ return func or (lambda f: f)
71
+ elif func:
72
+ # if we can, return single wrapped function
73
+ return wrapper(func)
74
+ else:
75
+ # otherwise, return decorator function
76
+ return wrapper
77
+
78
+ def async_decorator(self) -> Callable[..., Any] | None:
79
+ """
80
+ Decorator to apply the async_impl decorator to the wrapped function
81
+
82
+ Default implementation is a no-op
83
+ """
84
+ return None
85
+
86
+ def sync_ast_transform(
87
+ self, wrapped_node: ast.AST, transformers_globals: dict[str, Any]
88
+ ) -> ast.AST | None:
89
+ """
90
+ When this decorator is encountered in the ast during sync generation, this method is called
91
+ to transform the wrapped node.
92
+
93
+ If None is returned, the node will be dropped from the output file.
94
+
95
+ Args:
96
+ wrapped_node: ast node representing the wrapped function or class that is being wrapped
97
+ transformers_globals: the set of globals() from the transformers module. This is used to access
98
+ ast transformer classes that live outside the main codebase
99
+ Returns:
100
+ transformed ast node, or None if the node should be dropped
101
+ """
102
+ return wrapped_node
103
+
104
+ @classmethod
105
+ def get_for_node(cls, node: ast.Call | ast.Attribute | ast.Name) -> "AstDecorator":
106
+ """
107
+ Build an AstDecorator instance from an ast decorator node
108
+
109
+ The right subclass is found by comparing the string representation of the
110
+ decorator name to the class name. (Both names are converted to lowercase and
111
+ underscores are removed for comparison). If a matching subclass is found,
112
+ a new instance is created with the provided arguments.
113
+
114
+ Args:
115
+ node: ast.Call node representing the decorator
116
+ Returns:
117
+ AstDecorator instance corresponding to the decorator
118
+ Raises:
119
+ ValueError: if the decorator cannot be parsed
120
+ """
121
+ import ast
122
+
123
+ # expect decorators in format @CrossSync.<decorator_name>
124
+ # (i.e. should be an ast.Call or an ast.Attribute)
125
+ root_attr = node.func if isinstance(node, ast.Call) else node
126
+ if not isinstance(root_attr, ast.Attribute):
127
+ raise ValueError("Unexpected decorator format")
128
+ # extract the module and decorator names
129
+ if "CrossSync" in ast.dump(root_attr):
130
+ decorator_name = root_attr.attr
131
+ got_kwargs: dict[str, Any] = (
132
+ {str(kw.arg): cls._convert_ast_to_py(kw.value) for kw in node.keywords}
133
+ if hasattr(node, "keywords")
134
+ else {}
135
+ )
136
+ got_args = (
137
+ [cls._convert_ast_to_py(arg) for arg in node.args]
138
+ if hasattr(node, "args")
139
+ else []
140
+ )
141
+ # convert to standardized representation
142
+ formatted_name = decorator_name.replace("_", "").lower()
143
+ for subclass in cls.get_subclasses():
144
+ if subclass.__name__.lower() == formatted_name:
145
+ return subclass(*got_args, **got_kwargs)
146
+ raise ValueError(f"Unknown decorator encountered: {decorator_name}")
147
+ else:
148
+ raise ValueError("Not a CrossSync decorator")
149
+
150
+ @classmethod
151
+ def get_subclasses(cls) -> Iterable[type["AstDecorator"]]:
152
+ """
153
+ Get all subclasses of AstDecorator
154
+
155
+ Returns:
156
+ list of all subclasses of AstDecorator
157
+ """
158
+ for subclass in cls.__subclasses__():
159
+ yield from subclass.get_subclasses()
160
+ yield subclass
161
+
162
+ @classmethod
163
+ def _convert_ast_to_py(cls, ast_node: ast.expr | None) -> Any:
164
+ """
165
+ Helper to convert ast primitives to python primitives. Used when unwrapping arguments
166
+ """
167
+ import ast
168
+
169
+ if ast_node is None:
170
+ return None
171
+ if isinstance(ast_node, ast.Constant):
172
+ return ast_node.value
173
+ if isinstance(ast_node, ast.List):
174
+ return [cls._convert_ast_to_py(node) for node in ast_node.elts]
175
+ if isinstance(ast_node, ast.Tuple):
176
+ return tuple(cls._convert_ast_to_py(node) for node in ast_node.elts)
177
+ if isinstance(ast_node, ast.Dict):
178
+ return {
179
+ cls._convert_ast_to_py(k): cls._convert_ast_to_py(v)
180
+ for k, v in zip(ast_node.keys, ast_node.values)
181
+ }
182
+ raise ValueError(f"Unsupported type {type(ast_node)}")
183
+
184
+
185
+ class ConvertClass(AstDecorator):
186
+ """
187
+ Class decorator for guiding generation of sync classes
188
+
189
+ Args:
190
+ sync_name: use a new name for the sync class
191
+ replace_symbols: a dict of symbols and replacements to use when generating sync class
192
+ docstring_format_vars: a dict of variables to replace in the docstring
193
+ rm_aio: if True, automatically strip all asyncio keywords from method. If false,
194
+ only keywords wrapped in CrossSync.rm_aio() calls to be removed.
195
+ add_mapping_for_name: when given, will add a new attribute to CrossSync,
196
+ so the original class and its sync version can be accessed from CrossSync.<name>
197
+ """
198
+
199
+ def __init__(
200
+ self,
201
+ sync_name: str | None = None,
202
+ *,
203
+ replace_symbols: dict[str, str] | None = None,
204
+ docstring_format_vars: dict[str, tuple[str | None, str | None]] | None = None,
205
+ rm_aio: bool = False,
206
+ add_mapping_for_name: str | None = None,
207
+ ):
208
+ self.sync_name = sync_name
209
+ self.replace_symbols = replace_symbols
210
+ docstring_format_vars = docstring_format_vars or {}
211
+ self.async_docstring_format_vars = {
212
+ k: v[0] or "" for k, v in docstring_format_vars.items()
213
+ }
214
+ self.sync_docstring_format_vars = {
215
+ k: v[1] or "" for k, v in docstring_format_vars.items()
216
+ }
217
+ self.rm_aio = rm_aio
218
+ self.add_mapping_for_name = add_mapping_for_name
219
+
220
+ def async_decorator(self):
221
+ """
222
+ Use async decorator as a hook to update CrossSync mappings
223
+ """
224
+ from .cross_sync import CrossSync
225
+
226
+ if not self.add_mapping_for_name and not self.async_docstring_format_vars:
227
+ # return None if no changes needed
228
+ return None
229
+
230
+ new_mapping = self.add_mapping_for_name
231
+
232
+ def decorator(cls):
233
+ if new_mapping:
234
+ CrossSync.add_mapping(new_mapping, cls)
235
+ if self.async_docstring_format_vars:
236
+ cls.__doc__ = cls.__doc__.format(**self.async_docstring_format_vars)
237
+ return cls
238
+
239
+ return decorator
240
+
241
+ def sync_ast_transform(self, wrapped_node, transformers_globals):
242
+ """
243
+ Transform async class into sync copy
244
+ """
245
+ import ast
246
+ import copy
247
+
248
+ # copy wrapped node
249
+ wrapped_node = copy.deepcopy(wrapped_node)
250
+ # update name
251
+ if self.sync_name:
252
+ wrapped_node.name = self.sync_name
253
+ # strip CrossSync decorators
254
+ if hasattr(wrapped_node, "decorator_list"):
255
+ wrapped_node.decorator_list = [
256
+ d for d in wrapped_node.decorator_list if "CrossSync" not in ast.dump(d)
257
+ ]
258
+ else:
259
+ wrapped_node.decorator_list = []
260
+ # strip async keywords if specified
261
+ if self.rm_aio:
262
+ wrapped_node = transformers_globals["AsyncToSync"]().visit(wrapped_node)
263
+ # add mapping decorator if needed
264
+ if self.add_mapping_for_name:
265
+ wrapped_node.decorator_list.append(
266
+ ast.Call(
267
+ func=ast.Attribute(
268
+ value=ast.Name(id="CrossSync", ctx=ast.Load()),
269
+ attr="add_mapping_decorator",
270
+ ctx=ast.Load(),
271
+ ),
272
+ args=[
273
+ ast.Constant(value=self.add_mapping_for_name),
274
+ ],
275
+ keywords=[],
276
+ )
277
+ )
278
+ # replace symbols if specified
279
+ if self.replace_symbols:
280
+ wrapped_node = transformers_globals["SymbolReplacer"](
281
+ self.replace_symbols
282
+ ).visit(wrapped_node)
283
+ # update docstring if specified
284
+ if self.sync_docstring_format_vars:
285
+ docstring = ast.get_docstring(wrapped_node)
286
+ if docstring:
287
+ wrapped_node.body[0].value = ast.Constant(
288
+ value=docstring.format(**self.sync_docstring_format_vars)
289
+ )
290
+ return wrapped_node
291
+
292
+
293
+ class Convert(ConvertClass):
294
+ """
295
+ Method decorator to mark async methods to be converted to sync methods
296
+
297
+ Args:
298
+ sync_name: use a new name for the sync method
299
+ replace_symbols: a dict of symbols and replacements to use when generating sync method
300
+ docstring_format_vars: a dict of variables to replace in the docstring
301
+ rm_aio: if True, automatically strip all asyncio keywords from method. If False,
302
+ only the signature `async def` is stripped. Other keywords must be wrapped in
303
+ CrossSync.rm_aio() calls to be removed.
304
+ """
305
+
306
+ def __init__(
307
+ self,
308
+ sync_name: str | None = None,
309
+ *,
310
+ replace_symbols: dict[str, str] | None = None,
311
+ docstring_format_vars: dict[str, tuple[str | None, str | None]] | None = None,
312
+ rm_aio: bool = True,
313
+ ):
314
+ super().__init__(
315
+ sync_name=sync_name,
316
+ replace_symbols=replace_symbols,
317
+ docstring_format_vars=docstring_format_vars,
318
+ rm_aio=rm_aio,
319
+ add_mapping_for_name=None,
320
+ )
321
+
322
+ def sync_ast_transform(self, wrapped_node, transformers_globals):
323
+ """
324
+ Transform async method into sync
325
+ """
326
+ import ast
327
+
328
+ # replace async function with sync function
329
+ converted = ast.copy_location(
330
+ ast.FunctionDef(
331
+ wrapped_node.name,
332
+ wrapped_node.args,
333
+ wrapped_node.body,
334
+ wrapped_node.decorator_list
335
+ if hasattr(wrapped_node, "decorator_list")
336
+ else [],
337
+ wrapped_node.returns if hasattr(wrapped_node, "returns") else None,
338
+ ),
339
+ wrapped_node,
340
+ )
341
+ # transform based on arguments
342
+ return super().sync_ast_transform(converted, transformers_globals)
343
+
344
+
345
+ class Drop(AstDecorator):
346
+ """
347
+ Method decorator to drop methods or classes from the sync output
348
+ """
349
+
350
+ def sync_ast_transform(self, wrapped_node, transformers_globals):
351
+ """
352
+ Drop from sync output
353
+ """
354
+ return None
355
+
356
+
357
+ class Pytest(AstDecorator):
358
+ """
359
+ Used in place of pytest.mark.asyncio to mark tests
360
+
361
+ When generating sync version, also runs rm_aio to remove async keywords from
362
+ entire test function
363
+
364
+ Args:
365
+ rm_aio: if True, automatically strip all asyncio keywords from test code.
366
+ Defaults to True, to simplify test code generation.
367
+ """
368
+
369
+ def __init__(self, rm_aio=True):
370
+ self.rm_aio = rm_aio
371
+
372
+ def async_decorator(self):
373
+ import pytest
374
+
375
+ return pytest.mark.asyncio
376
+
377
+ def sync_ast_transform(self, wrapped_node, transformers_globals):
378
+ """
379
+ convert async to sync
380
+ """
381
+ import ast
382
+
383
+ # always convert method to sync
384
+ converted = ast.copy_location(
385
+ ast.FunctionDef(
386
+ wrapped_node.name,
387
+ wrapped_node.args,
388
+ wrapped_node.body,
389
+ wrapped_node.decorator_list
390
+ if hasattr(wrapped_node, "decorator_list")
391
+ else [],
392
+ wrapped_node.returns if hasattr(wrapped_node, "returns") else None,
393
+ ),
394
+ wrapped_node,
395
+ )
396
+ # convert entire body to sync if rm_aio is set
397
+ if self.rm_aio:
398
+ converted = transformers_globals["AsyncToSync"]().visit(converted)
399
+ return converted
400
+
401
+
402
+ class PytestFixture(AstDecorator):
403
+ """
404
+ Used in place of pytest.fixture or pytest.mark.asyncio to mark fixtures
405
+
406
+ Args:
407
+ *args: all arguments to pass to pytest.fixture
408
+ **kwargs: all keyword arguments to pass to pytest.fixture
409
+ """
410
+
411
+ def __init__(self, *args, **kwargs):
412
+ self._args = args
413
+ self._kwargs = kwargs
414
+
415
+ def async_decorator(self):
416
+ import pytest_asyncio # type: ignore
417
+
418
+ return lambda f: pytest_asyncio.fixture(*self._args, **self._kwargs)(f)
419
+
420
+ def sync_ast_transform(self, wrapped_node, transformers_globals):
421
+ import ast
422
+ import copy
423
+
424
+ new_node = copy.deepcopy(wrapped_node)
425
+ if not hasattr(new_node, "decorator_list"):
426
+ new_node.decorator_list = []
427
+ new_node.decorator_list.append(
428
+ ast.Call(
429
+ func=ast.Attribute(
430
+ value=ast.Name(id="pytest", ctx=ast.Load()),
431
+ attr="fixture",
432
+ ctx=ast.Load(),
433
+ ),
434
+ args=[ast.Constant(value=a) for a in self._args],
435
+ keywords=[
436
+ ast.keyword(arg=k, value=ast.Constant(value=v))
437
+ for k, v in self._kwargs.items()
438
+ ],
439
+ )
440
+ )
441
+ return new_node
@@ -0,0 +1,64 @@
1
+ # Copyright 2024 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from __future__ import annotations
15
+ from typing import Any
16
+
17
+
18
+ class MappingMeta(type):
19
+ """
20
+ Metaclass to provide add_mapping functionality, allowing users to add
21
+ custom attributes to derived classes at runtime.
22
+
23
+ Using a metaclass allows us to share functionality between CrossSync
24
+ and CrossSync._Sync_Impl, and it works better with mypy checks than
25
+ monkypatching
26
+ """
27
+
28
+ # list of attributes that can be added to the derived class at runtime
29
+ _runtime_replacements: dict[tuple[MappingMeta, str], Any] = {}
30
+
31
+ def add_mapping(cls: MappingMeta, name: str, value: Any):
32
+ """
33
+ Add a new attribute to the class, for replacing library-level symbols
34
+
35
+ Raises:
36
+ - AttributeError if the attribute already exists with a different value
37
+ """
38
+ key = (cls, name)
39
+ old_value = cls._runtime_replacements.get(key)
40
+ if old_value is None:
41
+ cls._runtime_replacements[key] = value
42
+ elif old_value != value:
43
+ raise AttributeError(f"Conflicting assignments for CrossSync.{name}")
44
+
45
+ def add_mapping_decorator(cls: MappingMeta, name: str):
46
+ """
47
+ Exposes add_mapping as a class decorator
48
+ """
49
+
50
+ def decorator(wrapped_cls):
51
+ cls.add_mapping(name, wrapped_cls)
52
+ return wrapped_cls
53
+
54
+ return decorator
55
+
56
+ def __getattr__(cls: MappingMeta, name: str):
57
+ """
58
+ Retrieve custom attributes
59
+ """
60
+ key = (cls, name)
61
+ found = cls._runtime_replacements.get(key)
62
+ if found is not None:
63
+ return found
64
+ raise AttributeError(f"CrossSync has no attribute {name}")