Skip to content

Commit d18b8ee

Browse files
Migrate python formatting tool from yapf to ruff and use uv in CI. (#1265)
Changes included: * Replace yapf with ruff in Python/requirements.txt and Python/pyproject.toml. * Rewrite python formatter Python/pyfmt.py to use `ruff format -`. * Format all Python code under Python/. * Update CI at .github/workflows/test_python.yml to install uv and use `uv pip install --system` for speed. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent c6df1ef commit d18b8ee

File tree

17 files changed

+274
-186
lines changed

17 files changed

+274
-186
lines changed

.github/workflows/test_python.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ jobs:
4444
python-version: ${{ matrix.python-version }}
4545
- name: Install dependencies
4646
working-directory: ./Python
47-
run: pip install -r requirements.txt
47+
run: |
48+
pip install uv
49+
uv pip install --system -r requirements.txt
4850
- name: Lint
4951
working-directory: ./Python
5052
run: python pyfmt.py --check_only --exclude "**/venv/**/*.py" **/*.py

Python/alerts-to-discord/functions/main.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
DISCORD_WEBHOOK_URL = params.SecretParam("DISCORD_WEBHOOK_URL")
2525

2626

27-
def post_message_to_discord(bot_name: str, message_body: str,
28-
webhook_url: str) -> requests.Response:
27+
def post_message_to_discord(
28+
bot_name: str, message_body: str, webhook_url: str
29+
) -> requests.Response:
2930
"""Posts a message to Discord with Discord's Webhook API.
3031
3132
Params:
@@ -36,16 +37,18 @@ def post_message_to_discord(bot_name: str, message_body: str,
3637
raise EnvironmentError(
3738
"No webhook URL found. Set the Discord Webhook URL before deploying. "
3839
"Learn more about Discord webhooks here: "
39-
"https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks")
40+
"https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"
41+
)
4042

4143
return requests.post(
4244
url=webhook_url,
4345
json={
4446
# Here's what the Discord API supports in the payload:
4547
# https://discord.com/developers/docs/resources/webhook#execute-webhook-jsonform-params
4648
"username": bot_name,
47-
"content": message_body
48-
})
49+
"content": message_body,
50+
},
51+
)
4952

5053

5154
# [START v2Alerts]
@@ -110,7 +113,8 @@ def post_new_udid_to_discord(event: app_distribution_fn.NewTesterDeviceEvent) ->
110113
except (EnvironmentError, requests.HTTPError) as error:
111114
print(
112115
f"Unable to post iOS device registration alert for {app_dist.tester_email} to Discord.",
113-
error)
116+
error,
117+
)
114118

115119

116120
# [START v2PerformanceAlertTrigger]
@@ -139,8 +143,9 @@ def post_performance_alert_to_discord(event: performance_fn.PerformanceThreshold
139143

140144
try:
141145
# [START v2SendPerformanceAlertToDiscord]
142-
response = post_message_to_discord("App Performance Bot", message,
143-
DISCORD_WEBHOOK_URL.value)
146+
response = post_message_to_discord(
147+
"App Performance Bot", message, DISCORD_WEBHOOK_URL.value
148+
)
144149
if response.ok:
145150
print(f"Posted Firebase Performance alert {perf.event_name} to Discord.")
146151
pprint.pp(event.data.payload)

Python/fcm-notifications/functions/main.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def send_follower_notification(event: db_fn.Event[db_fn.Change]) -> None:
2525
print(f"User {follower_uid} is now following user {followed_uid}")
2626
tokens_ref = db.reference(f"users/{followed_uid}/notificationTokens")
2727
notification_tokens = tokens_ref.get()
28-
if (not isinstance(notification_tokens, dict) or len(notification_tokens) < 1):
28+
if not isinstance(notification_tokens, dict) or len(notification_tokens) < 1:
2929
print("There are no tokens to send notifications to.")
3030
return
3131
print(f"There are {len(notification_tokens)} tokens to send notifications to.")
@@ -52,6 +52,8 @@ def send_follower_notification(event: db_fn.Event[db_fn.Change]) -> None:
5252
if not isinstance(exception, exceptions.FirebaseError):
5353
continue
5454
message = exception.http_response.json()["error"]["message"]
55-
if (isinstance(exception, messaging.UnregisteredError) or
56-
message == "The registration token is not a valid FCM registration token"):
55+
if (
56+
isinstance(exception, messaging.UnregisteredError)
57+
or message == "The registration token is not a valid FCM registration token"
58+
):
5759
tokens_ref.child(msgs[i].token).delete()

Python/post-signup-event/functions/main.py

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
# [START savegoogletoken]
3232
@identity_fn.before_user_created()
3333
def savegoogletoken(
34-
event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeCreateResponse | None:
34+
event: identity_fn.AuthBlockingEvent,
35+
) -> identity_fn.BeforeCreateResponse | None:
3536
"""During sign-up, save the Google OAuth2 access token and queue up a task
3637
to schedule an onboarding session on the user's Google Calendar.
3738
@@ -48,24 +49,19 @@ def savegoogletoken(
4849
doc_ref.set({"calendar_access_token": event.credential.access_token}, merge=True)
4950

5051
tasks_client = google.cloud.tasks_v2.CloudTasksClient()
51-
task_queue = tasks_client.queue_path(params.PROJECT_ID.value,
52-
options.SupportedRegion.US_CENTRAL1,
53-
"scheduleonboarding")
52+
task_queue = tasks_client.queue_path(
53+
params.PROJECT_ID.value, options.SupportedRegion.US_CENTRAL1, "scheduleonboarding"
54+
)
5455
target_uri = get_function_url("scheduleonboarding")
55-
calendar_task = google.cloud.tasks_v2.Task(http_request={
56-
"http_method": google.cloud.tasks_v2.HttpMethod.POST,
57-
"url": target_uri,
58-
"headers": {
59-
"Content-type": "application/json"
56+
calendar_task = google.cloud.tasks_v2.Task(
57+
http_request={
58+
"http_method": google.cloud.tasks_v2.HttpMethod.POST,
59+
"url": target_uri,
60+
"headers": {"Content-type": "application/json"},
61+
"body": json.dumps({"data": {"uid": event.data.uid}}).encode(),
6062
},
61-
"body": json.dumps({
62-
"data": {
63-
"uid": event.data.uid
64-
}
65-
}).encode()
66-
},
67-
schedule_time=datetime.now() +
68-
timedelta(minutes=1))
63+
schedule_time=datetime.now() + timedelta(minutes=1),
64+
)
6965
tasks_client.create_task(parent=task_queue, task=calendar_task)
7066
# [END savegoogletoken]
7167

@@ -79,46 +75,48 @@ def scheduleonboarding(request: tasks_fn.CallableRequest) -> https_fn.Response:
7975
"""
8076

8177
if "uid" not in request.data:
82-
return https_fn.Response(status=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
83-
response="No user specified.")
78+
return https_fn.Response(
79+
status=https_fn.FunctionsErrorCode.INVALID_ARGUMENT, response="No user specified."
80+
)
8481
uid = request.data["uid"]
8582

8683
user_record: auth.UserRecord = auth.get_user(uid)
8784
if user_record.email is None:
88-
return https_fn.Response(status=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
89-
response="No email address on record.")
85+
return https_fn.Response(
86+
status=https_fn.FunctionsErrorCode.INVALID_ARGUMENT,
87+
response="No email address on record.",
88+
)
9089

9190
firestore_client: google.cloud.firestore.Client = firestore.client()
9291
user_info = firestore_client.collection("user_info").document(uid).get().to_dict()
9392
if not isinstance(user_info, dict) or "calendar_access_token" not in user_info:
94-
return https_fn.Response(status=https_fn.FunctionsErrorCode.PERMISSION_DENIED,
95-
response="No Google OAuth token found.")
93+
return https_fn.Response(
94+
status=https_fn.FunctionsErrorCode.PERMISSION_DENIED,
95+
response="No Google OAuth token found.",
96+
)
9697
calendar_access_token = user_info["calendar_access_token"]
9798
firestore_client.collection("user_info").document(uid).update(
98-
{"calendar_access_token": google.cloud.firestore.DELETE_FIELD})
99+
{"calendar_access_token": google.cloud.firestore.DELETE_FIELD}
100+
)
99101

100102
google_credentials = google.oauth2.credentials.Credentials(token=calendar_access_token)
101103

102-
calendar_client = googleapiclient.discovery.build("calendar",
103-
"v3",
104-
credentials=google_credentials)
104+
calendar_client = googleapiclient.discovery.build(
105+
"calendar", "v3", credentials=google_credentials
106+
)
105107
calendar_event = {
106108
"summary": "Onboarding with ExampleCo",
107109
"location": "Video call",
108110
"description": "Walk through onboarding tasks with an ExampleCo engineer.",
109111
"start": {
110112
"dateTime": (datetime.now() + timedelta(days=3)).isoformat(),
111-
"timeZone": "America/Los_Angeles"
113+
"timeZone": "America/Los_Angeles",
112114
},
113115
"end": {
114116
"dateTime": (datetime.now() + timedelta(days=3, hours=1)).isoformat(),
115-
"timeZone": "America/Los_Angeles"
117+
"timeZone": "America/Los_Angeles",
116118
},
117-
"attendees": [{
118-
"email": user_record.email
119-
}, {
120-
"email": "onboarding@example.com"
121-
}]
119+
"attendees": [{"email": user_record.email}, {"email": "onboarding@example.com"}],
122120
}
123121
calendar_client.events().insert(calendarId="primary", body=calendar_event).execute()
124122

@@ -137,10 +135,13 @@ def get_function_url(name: str, location: str = options.SupportedRegion.US_CENTR
137135
The URL of the function
138136
"""
139137
credentials, project_id = google.auth.default(
140-
scopes=["https://www.googleapis.com/auth/cloud-platform"])
138+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
139+
)
141140
authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
142-
url = ("https://cloudfunctions.googleapis.com/v2beta/" +
143-
f"projects/{project_id}/locations/{location}/functions/{name}")
141+
url = (
142+
"https://cloudfunctions.googleapis.com/v2beta/"
143+
+ f"projects/{project_id}/locations/{location}/functions/{name}"
144+
)
144145
response = authed_session.get(url)
145146
data = response.json()
146147
function_url = data["serviceConfig"]["uri"]

Python/pyfmt.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
import difflib
1818
import pathlib
1919
import re
20-
21-
from yapf.yapflib import yapf_api
20+
import subprocess
2221

2322
start_tag_re = re.compile(r"^([ \t]*)#\s*\[START\s+(\w+).*\]\s*\n", flags=re.MULTILINE)
2423
end_tag_re = re.compile(r"^\s*#\s*\[END\s+(\w+).*\][ \t]*$", flags=re.MULTILINE)
@@ -41,19 +40,25 @@ def check_and_diff(files: list[str]) -> int:
4140
orig = f.read()
4241
fmt = format(orig)
4342
diff = list(
44-
difflib.unified_diff(orig.splitlines(),
45-
fmt.splitlines(),
46-
fromfile=file,
47-
tofile=f"{file} (reformatted)",
48-
lineterm=""))
43+
difflib.unified_diff(
44+
orig.splitlines(),
45+
fmt.splitlines(),
46+
fromfile=file,
47+
tofile=f"{file} (reformatted)",
48+
lineterm="",
49+
)
50+
)
4951
if len(diff) > 0:
5052
diff_count += 1
5153
print("\n".join(diff), end="\n\n")
5254
return diff_count
5355

5456

5557
def format(src: str) -> str:
56-
out, _ = yapf_api.FormatCode(src, style_config=pyproject_toml)
58+
result = subprocess.run(
59+
["ruff", "format", "-"], input=src.encode("utf-8"), capture_output=True, check=True
60+
)
61+
out = result.stdout.decode("utf-8")
5762
out = fix_region_tags(out)
5863
return out
5964

@@ -83,15 +88,19 @@ def fix_end_tag(m: re.Match) -> str:
8388
import argparse
8489

8590
argparser = argparse.ArgumentParser()
86-
argparser.add_argument("--check_only",
87-
"-c",
88-
action="store_true",
89-
help="check files and print diffs, but don't modify files")
90-
argparser.add_argument("--exclude",
91-
"-e",
92-
action="append",
93-
default=[],
94-
help="exclude file or glob (can specify multiple times)")
91+
argparser.add_argument(
92+
"--check_only",
93+
"-c",
94+
action="store_true",
95+
help="check files and print diffs, but don't modify files",
96+
)
97+
argparser.add_argument(
98+
"--exclude",
99+
"-e",
100+
action="append",
101+
default=[],
102+
help="exclude file or glob (can specify multiple times)",
103+
)
95104
argparser.add_argument("file_or_glob", nargs="+")
96105
args = argparser.parse_args()
97106

Python/pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
[project]
1616
name = "functions_samples"
1717
version = "1.0.0"
18-
[tool.yapf]
19-
based_on_style = "google"
20-
column_limit = 100
18+
[tool.ruff]
19+
line-length = 100
2120
[tool.mypy]
2221
python_version = "3.10"

0 commit comments

Comments
 (0)