Skip to content

Commit d2a01d3

Browse files
committed
Merge remote-tracking branch 'remote.googleapis/python-bigtable/main' into migration.python-bigtable.migration.2025-11-25_16-09-19.migrate
2 parents 0737369 + 391f563 commit d2a01d3

File tree

672 files changed

+247901
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

672 files changed

+247901
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright 2020 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
# Generated by synthtool. DO NOT EDIT!
18+
[run]
19+
branch = True
20+
omit =
21+
google/cloud/bigtable_admin/__init__.py
22+
google/cloud/bigtable_admin/gapic_version.py
23+
24+
[report]
25+
fail_under = 99
26+
show_missing = True
27+
exclude_lines =
28+
# Re-enable the standard pragma
29+
pragma: NO COVER
30+
# Ignore debug-only repr
31+
def __repr__
32+
# Ignore abstract methods
33+
raise NotImplementedError
34+
omit =
35+
*/site-packages/*.py
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# CrossSync
2+
3+
CrossSync provides a simple way to share logic between async and sync code.
4+
It is made up of a small library that provides:
5+
1. a set of shims that provide a shared sync/async API surface
6+
2. annotations that are used to guide generation of a sync version from an async class
7+
8+
Using CrossSync, the async code is treated as the source of truth, and sync code is generated from it.
9+
10+
## Usage
11+
12+
### CrossSync Shims
13+
14+
Many Asyncio components have direct, 1:1 threaded counterparts for use in non-asyncio code. CrossSync
15+
provides a compatibility layer that works with both
16+
17+
| CrossSync | Asyncio Version | Sync Version |
18+
| --- | --- | --- |
19+
| CrossSync.Queue | asyncio.Queue | queue.Queue |
20+
| CrossSync.Condition | asyncio.Condition | threading.Condition |
21+
| CrossSync.Future | asyncio.Future | Concurrent.futures.Future |
22+
| CrossSync.Task | asyncio.Task | Concurrent.futures.Future |
23+
| CrossSync.Event | asyncio.Event | threading.Event |
24+
| CrossSync.Semaphore | asyncio.Semaphore | threading.Semaphore |
25+
| CrossSync.Awaitable | typing.Awaitable | typing.Union (no-op type) |
26+
| CrossSync.Iterable | typing.AsyncIterable | typing.Iterable |
27+
| CrossSync.Iterator | typing.AsyncIterator | typing.Iterator |
28+
| CrossSync.Generator | typing.AsyncGenerator | typing.Generator |
29+
| CrossSync.Retry | google.api_core.retry.AsyncRetry | google.api_core.retry.Retry |
30+
| CrossSync.StopIteration | StopAsyncIteration | StopIteration |
31+
| CrossSync.Mock | unittest.mock.AsyncMock | unittest.mock.Mock |
32+
33+
Custom aliases can be added using `CrossSync.add_mapping(class, name)`
34+
35+
Additionally, CrossSync provides method implementations that work equivalently in async and sync code:
36+
- `CrossSync.sleep()`
37+
- `CrossSync.gather_partials()`
38+
- `CrossSync.wait()`
39+
- `CrossSync.condition_wait()`
40+
- `CrossSync,event_wait()`
41+
- `CrossSync.create_task()`
42+
- `CrossSync.retry_target()`
43+
- `CrossSync.retry_target_stream()`
44+
45+
### Annotations
46+
47+
CrossSync provides a set of annotations to mark up async classes, to guide the generation of sync code.
48+
49+
- `@CrossSync.convert_sync`
50+
- marks classes for conversion. Unmarked classes will be copied as-is
51+
- if add_mapping is included, the async and sync classes can be accessed using a shared CrossSync.X alias
52+
- `@CrossSync.convert`
53+
- marks async functions for conversion. Unmarked methods will be copied as-is
54+
- `@CrossSync.drop`
55+
- marks functions or classes that should not be included in sync output
56+
- `@CrossSync.pytest`
57+
- marks test functions. Test functions automatically have all async keywords stripped (i.e., rm_aio is unneeded)
58+
- `CrossSync.add_mapping`
59+
- manually registers a new CrossSync.X alias, for custom types
60+
- `CrossSync.rm_aio`
61+
- Marks regions of the code that include asyncio keywords that should be stripped during generation
62+
63+
### Code Generation
64+
65+
Generation can be initiated using `nox -s generate_sync`
66+
from the root of the project. This will find all classes with the `__CROSS_SYNC_OUTPUT__ = "path/to/output"`
67+
annotation, and generate a sync version of classes marked with `@CrossSync.convert_sync` at the output path.
68+
69+
There is a unit test at `tests/unit/data/test_sync_up_to_date.py` that verifies that the generated code is up to date
70+
71+
## Architecture
72+
73+
CrossSync is made up of two parts:
74+
- the runtime shims and annotations live in `/google/cloud/bigtable/_cross_sync`
75+
- the code generation logic lives in `/.cross_sync/` in the repo root
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 Sequence
16+
import ast
17+
"""
18+
Entrypoint for initiating an async -> sync conversion using CrossSync
19+
20+
Finds all python files rooted in a given directory, and uses
21+
transformers.CrossSyncFileProcessor to handle any files marked with
22+
__CROSS_SYNC_OUTPUT__
23+
"""
24+
25+
26+
def extract_header_comments(file_path) -> str:
27+
"""
28+
Extract the file header. Header is defined as the top-level
29+
comments before any code or imports
30+
"""
31+
header = []
32+
with open(file_path, "r") as f:
33+
for line in f:
34+
if line.startswith("#") or line.strip() == "":
35+
header.append(line)
36+
else:
37+
break
38+
header.append("\n# This file is automatically generated by CrossSync. Do not edit manually.\n\n")
39+
return "".join(header)
40+
41+
42+
class CrossSyncOutputFile:
43+
44+
def __init__(self, output_path: str, ast_tree, header: str | None = None):
45+
self.output_path = output_path
46+
self.tree = ast_tree
47+
self.header = header or ""
48+
49+
def render(self, with_formatter=True, save_to_disk: bool = True) -> str:
50+
"""
51+
Render the file to a string, and optionally save to disk
52+
53+
Args:
54+
with_formatter: whether to run the output through black before returning
55+
save_to_disk: whether to write the output to the file path
56+
"""
57+
full_str = self.header + ast.unparse(self.tree)
58+
if with_formatter:
59+
import black # type: ignore
60+
import autoflake # type: ignore
61+
62+
full_str = black.format_str(
63+
autoflake.fix_code(full_str, remove_all_unused_imports=True),
64+
mode=black.FileMode(),
65+
)
66+
if save_to_disk:
67+
import os
68+
os.makedirs(os.path.dirname(self.output_path), exist_ok=True)
69+
with open(self.output_path, "w") as f:
70+
f.write(full_str)
71+
return full_str
72+
73+
74+
def convert_files_in_dir(directory: str) -> set[CrossSyncOutputFile]:
75+
import glob
76+
from transformers import CrossSyncFileProcessor
77+
78+
# find all python files in the directory
79+
files = glob.glob(directory + "/**/*.py", recursive=True)
80+
# keep track of the output files pointed to by the annotated classes
81+
artifacts: set[CrossSyncOutputFile] = set()
82+
file_transformer = CrossSyncFileProcessor()
83+
# run each file through ast transformation to find all annotated classes
84+
for file_path in files:
85+
ast_tree = ast.parse(open(file_path).read())
86+
output_path = file_transformer.get_output_path(ast_tree)
87+
if output_path is not None:
88+
# contains __CROSS_SYNC_OUTPUT__ annotation
89+
converted_tree = file_transformer.visit(ast_tree)
90+
header = extract_header_comments(file_path)
91+
artifacts.add(CrossSyncOutputFile(output_path, converted_tree, header))
92+
# return set of output artifacts
93+
return artifacts
94+
95+
96+
def save_artifacts(artifacts: Sequence[CrossSyncOutputFile]):
97+
for a in artifacts:
98+
a.render(save_to_disk=True)
99+
100+
101+
if __name__ == "__main__":
102+
import sys
103+
104+
search_root = sys.argv[1]
105+
outputs = convert_files_in_dir(search_root)
106+
print(f"Generated {len(outputs)} artifacts: {[a.output_path for a in outputs]}")
107+
save_artifacts(outputs)

0 commit comments

Comments
 (0)