Skip to content

Commit a54b2c7

Browse files
committed
Substitute template variables in parallel
1 parent 22ed697 commit a54b2c7

2 files changed

Lines changed: 57 additions & 3 deletions

File tree

atr/construct.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import dataclasses
1919
import datetime
2020
import hashlib
21+
import re
2122
from collections.abc import Mapping
2223
from typing import Final, Literal, TypedDict, overload
2324

@@ -413,6 +414,8 @@ def _substitute(text: str, values: _VoteSubjectValues, context: Literal["vote_su
413414
def _substitute(text: str, values: _VoteValues, context: Literal["vote"]) -> str: ...
414415
def _substitute(text: str, values: Mapping[str, object], context: Context) -> str:
415416
_ = context # marks as unused for pyright - we're using the value to pick the right overload
416-
for name, value in values.items():
417-
text = text.replace(f"{{{{{name}}}}}", str(value))
418-
return text
417+
if not values:
418+
return text
419+
names = "|".join(re.escape(name) for name in values)
420+
pattern = re.compile(r"\{\{(" + names + r")\}\}")
421+
return pattern.sub(lambda match: str(values[match.group(1)]), text)

tests/unit/test_construct_announce.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,47 @@ def revision(self, **kwargs: object) -> MockQuery:
4949
return MockQuery(self._revision)
5050

5151

52+
@pytest.mark.asyncio
53+
async def test_announce_release_subject_and_body_does_not_expand_injected_tag(monkeypatch) -> None:
54+
committee = SimpleNamespace(key="myproject", is_podling=False, display_name="Apache MyProject")
55+
project = SimpleNamespace(
56+
short_display_name="Apache MyProject",
57+
name="MyProject",
58+
key="myproject",
59+
bug_database="bug_database",
60+
download_page="download_page",
61+
homepage="homepage",
62+
lifecycle_page="lifecycle_page",
63+
mailing_lists="mailing_lists",
64+
repository="repository",
65+
)
66+
release = SimpleNamespace(key="myproject-1.0.0", committee=committee, project=project)
67+
revision = SimpleNamespace(number="1", tag="{{YOUR_FULL_NAME}}")
68+
69+
monkeypatch.setattr(
70+
construct.config,
71+
"get",
72+
lambda: SimpleNamespace(APP_HOST="downloads.apache.org", SVN_PUBLISH_URL=None),
73+
)
74+
monkeypatch.setattr(construct.db, "session", _mock_session_factory(MockDBSession(release, revision)))
75+
76+
_subject, body = await construct.announce_release_subject_and_body(
77+
"{{PROJECT_NAME}} {{VERSION}}",
78+
"Tag: {{TAG}}\nName: {{YOUR_FULL_NAME}}",
79+
construct.AnnounceReleaseOptions(
80+
asfuid="example",
81+
fullname="Example User",
82+
project_key=safe.ProjectKey("myproject"),
83+
version_key=safe.VersionKey("1.0.0"),
84+
revision_number=safe.RevisionNumber("1"),
85+
),
86+
)
87+
88+
tag_line, name_line = body.split("\n")
89+
assert tag_line == "Tag: {{YOUR_FULL_NAME}}"
90+
assert name_line == "Name: Example User"
91+
92+
5293
@pytest.mark.asyncio
5394
async def test_announce_release_subject_and_body_uses_podling_downloads_url(monkeypatch) -> None:
5495
committee = SimpleNamespace(key="myproject", is_podling=True, display_name="Apache MyProject (podling)")
@@ -129,6 +170,16 @@ async def test_announce_release_subject_and_body_uses_top_level_downloads_url(mo
129170
assert body == "https://downloads.apache.org/downloads/myproject/"
130171

131172

173+
def test_substitute_does_not_rescan_replacement_values() -> None:
174+
result = construct._substitute(
175+
"{{PROJECT_NAME}} {{VERSION}} {{UNKNOWN}}",
176+
{"PROJECT_NAME": "{{VERSION}}", "VERSION": "1.0.0"},
177+
"announce_subject",
178+
)
179+
180+
assert result == "{{VERSION}} 1.0.0 {{UNKNOWN}}"
181+
182+
132183
def _mock_session_factory(data: MockDBSession):
133184
@contextlib.asynccontextmanager
134185
async def _session():

0 commit comments

Comments
 (0)