Skip to content

Commit 3be34d0

Browse files
authored
Merge pull request #11 from pierrecomputer/make-async-lt-3.10-compatible
make `async with` < 3.10 compatible
2 parents bd26ef8 + 1b17c4f commit 3be34d0

4 files changed

Lines changed: 101 additions & 37 deletions

File tree

packages/code-storage-python/pierre_storage/commit.py

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -389,28 +389,26 @@ async def send(self) -> CommitResult:
389389
"Code-Storage-Agent": get_user_agent(),
390390
}
391391

392-
async with (
393-
httpx.AsyncClient() as client,
394-
client.stream(
392+
async with httpx.AsyncClient() as client:
393+
async with client.stream(
395394
"POST",
396395
self.url,
397396
headers=headers,
398397
content=self._build_request_body(metadata),
399398
timeout=180.0,
400-
) as response,
401-
):
402-
if not response.is_success:
403-
error_info = await _parse_commit_error(response, "createCommit")
404-
raise RefUpdateError(
405-
error_info["message"],
406-
status=error_info["status"],
407-
reason=error_info["status"],
408-
ref_update=error_info.get("ref_update"),
409-
)
410-
411-
result_data = await response.aread()
412-
result = json.loads(result_data)
413-
return _build_commit_result(result)
399+
) as response:
400+
if not response.is_success:
401+
error_info = await _parse_commit_error(response, "createCommit")
402+
raise RefUpdateError(
403+
error_info["message"],
404+
status=error_info["status"],
405+
reason=error_info["status"],
406+
ref_update=error_info.get("ref_update"),
407+
)
408+
409+
result_data = await response.aread()
410+
result = json.loads(result_data)
411+
return _build_commit_result(result)
414412

415413
def _build_metadata(self) -> Dict[str, Any]:
416414
"""Build metadata payload for commit."""
@@ -491,28 +489,26 @@ async def request_stream() -> AsyncIterator[bytes]:
491489

492490
url = f"{base_url.rstrip('/')}/api/v{api_version}/repos/diff-commit"
493491

494-
async with (
495-
httpx.AsyncClient() as client,
496-
client.stream(
492+
async with httpx.AsyncClient() as client:
493+
async with client.stream(
497494
"POST",
498495
url,
499496
headers=headers,
500497
content=request_stream(),
501498
timeout=180.0,
502-
) as response,
503-
):
504-
if not response.is_success:
505-
error_info = await _parse_commit_error(response, "createCommitFromDiff")
506-
raise RefUpdateError(
507-
error_info["message"],
508-
status=error_info["status"],
509-
reason=error_info["status"],
510-
ref_update=error_info.get("ref_update"),
511-
)
499+
) as response:
500+
if not response.is_success:
501+
error_info = await _parse_commit_error(response, "createCommitFromDiff")
502+
raise RefUpdateError(
503+
error_info["message"],
504+
status=error_info["status"],
505+
reason=error_info["status"],
506+
ref_update=error_info.get("ref_update"),
507+
)
512508

513-
result_data = await response.aread()
514-
result = json.loads(result_data)
515-
return _build_commit_result(result)
509+
result_data = await response.aread()
510+
result = json.loads(result_data)
511+
return _build_commit_result(result)
516512

517513

518514
def resolve_commit_ttl_seconds(options: Optional[CreateCommitOptions]) -> int:

packages/code-storage-python/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "pierre-storage"
7-
version = "1.3.2"
7+
version = "1.3.3"
88
description = "Pierre Git Storage SDK for Python"
99
readme = "README.md"
1010
license = "MIT"
@@ -68,7 +68,7 @@ target-version = "py39"
6868

6969
[tool.ruff.lint]
7070
select = ["E", "F", "I", "N", "W", "B", "C4", "SIM"]
71-
ignore = ["E501"]
71+
ignore = ["E501", "SIM117"]
7272

7373
[tool.uv]
7474
dev-dependencies = [

packages/code-storage-python/tests/test_commit.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,3 +676,71 @@ def capture_stream(*args, **kwargs):
676676
assert captured_headers is not None
677677
assert "Code-Storage-Agent" in captured_headers
678678
assert captured_headers["Code-Storage-Agent"] == get_user_agent()
679+
680+
@pytest.mark.asyncio
681+
async def test_send_importable_on_python39(self, git_storage_options: dict) -> None:
682+
"""Regression test: parenthesized async-with syntax is Python 3.10+ only.
683+
684+
Before the fix, commit.py used:
685+
686+
async with (
687+
httpx.AsyncClient() as client,
688+
client.stream(...) as response,
689+
):
690+
691+
That form raises SyntaxError on Python 3.9 at import time, making the
692+
entire module unusable despite pyproject.toml declaring requires-python>=3.9.
693+
The fix is nested async-with statements, which are valid back to Python 3.1.
694+
695+
This test confirms send() completes successfully, which would be unreachable
696+
on Python 3.9 because the module would never import.
697+
"""
698+
import sys
699+
700+
import pierre_storage.commit # must be importable without SyntaxError
701+
702+
assert pierre_storage.commit is not None, (
703+
"pierre_storage.commit failed to import — likely a SyntaxError "
704+
f"from parenthesized async-with on Python {sys.version_info}"
705+
)
706+
707+
storage = GitStorage(git_storage_options)
708+
709+
stream_response = MagicMock()
710+
stream_response.is_success = True
711+
stream_response.aread = AsyncMock(
712+
return_value=(
713+
b'{"commit":{"commit_sha":"aaa","tree_sha":"bbb",'
714+
b'"target_branch":"main","pack_bytes":10,"blob_count":1},'
715+
b'"result":{"success":true,"status":"ok","branch":"main",'
716+
b'"old_sha":"000","new_sha":"aaa"}}'
717+
)
718+
)
719+
720+
with patch("httpx.AsyncClient") as mock_client:
721+
mock_response = MagicMock()
722+
mock_response.status_code = 200
723+
mock_response.is_success = True
724+
mock_response.json.return_value = {"repo_id": "test-repo"}
725+
mock_client.return_value.__aenter__.return_value.post = AsyncMock(
726+
return_value=mock_response
727+
)
728+
stream_ctx = MagicMock()
729+
stream_ctx.__aenter__ = AsyncMock(return_value=stream_response)
730+
stream_ctx.__aexit__ = AsyncMock(return_value=None)
731+
mock_client.return_value.__aenter__.return_value.stream = MagicMock(
732+
return_value=stream_ctx
733+
)
734+
735+
repo = await storage.create_repo(id="test-repo")
736+
result = await (
737+
repo.create_commit(
738+
target_branch="main",
739+
commit_message="test",
740+
author={"name": "A", "email": "a@example.com"},
741+
)
742+
.add_file_from_string("f.txt", "hello")
743+
.send()
744+
)
745+
746+
assert result["commit_sha"] == "aaa"

packages/code-storage-python/uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)