From aiobotocore-bot
Ports synchronous botocore tests to async aiobotocore counterparts under tests/botocore_tests, validates each with pytest -x, commits on pass or reverts on fail. Handles diff-based syncs or backfill of missing tests.
npx claudepluginhub aio-libs/aiobotocore --plugin aiobotocore-botThis skill is limited to using the following tools:
Port botocore tests to aiobotocore. Two driving scenarios:
Classifies botocore version bumps as no-port, port-required, or ambiguous for aiobotocore. Diffs git tags in botocore clone, checks overrides, async methods, and aio classes for changes.
Implements Python asyncio patterns for concurrent I/O, async web APIs (FastAPI, aiohttp), web scrapers, real-time apps, and non-blocking systems.
Provides Python asyncio and async/await patterns for building async web APIs (FastAPI, aiohttp, Sanic), concurrent I/O operations, web scrapers, and real-time apps.
Share bugs, ideas, or general feedback.
Port botocore tests to aiobotocore. Two driving scenarios:
--from=X --to=Y): called from botocore-sync-prompt.md Step 5
(port path). For each test file under botocore/tests/ that has new or changed tests in the
X..Y range AND has a mirror in tests/botocore_tests/, apply the sync→async conversion
to the new/changed tests and write them into the mirror.--backfill): human-invoked when maintainers want to narrow the coverage
gap. Walk every botocore/tests/<unit|functional>/test_*.py that has a partial aiobotocore
mirror (same filename in tests/botocore_tests/<unit|functional>/) and port any test function
missing from the mirror. Scoped so it doesn't silently port hundreds of tests at once — run
with --paths= to limit to specific files.Conversion rules are identical in both modes. The skill writes files, validates each with
pytest -x, and only commits what passes. Failures are preserved as advisory output so the
human reviewer can finish by hand.
--from=<version>, --to=<version> (port-PR mode): botocore tag range. Used to diff for
added/changed tests.--backfill (backfill mode): port every not-yet-ported test function from existing mirrored
files, not just those changed in a version range.--paths=<file1,file2,...> (optional): restrict to specific test file basenames (e.g.
test_credentials.py,test_signers.py). Required in --backfill mode to avoid wall-of-
churn PRs. In --from/--to mode, defaults to "every file that appears in the diff".--dry-run (optional): emit the converted diffs to stdout; do not write files. Useful for
review before letting the skill commit changes.The modes are mutually exclusive: pass --from/--to OR --backfill, not both.
Port-PR mode (--from/--to):
git -C $BOTOCORE_CLONE diff --name-only $FROM..$TO -- 'botocore/tests/**/test_*.py'
For each changed file, decide whether it's in scope via this order:
tests/botocore_tests/<unit|functional>/<same-name> → in scope,
port new/changed tests into that mirror.overrides registry (any form:
ClassName.method, bare function, or bare class), ORAioX counterpart is in the
aio_classes registry, ORAioX subclasses X — the test
validates inherited behavior of the aiobotocore subclass too).When creating a new mirror file (case 2), bring only the test functions + their direct dependencies (module-level constants, imported helpers used in the port body). Do NOT copy the whole botocore file — that drags in fixtures and tests the aiobotocore surface doesn't exercise.
Backfill mode:
find tests/botocore_tests -name 'test_*.py' -type f
For each aiobotocore test file, identify its botocore counterpart. Use that file pair as the scope. Backfill mode does not create new mirror files — it only fills gaps in existing ones.
For each candidate file:
uv run python -c "import ast; ...") to extract every
top-level def test_* and every class Test*/def test_* method.Skip these categories regardless of mode:
overrides registry from
scripts/registries.py to identify which tests exercise overridden code.test_*_sync_* or that import from botocore.httpsession directly — the sync
HTTP path doesn't exist in aiobotocore.For each test function to port, apply these transformations in order:
def test_X(...) → async def test_X(...)class TestX staying as a class is fine; convert its def test_* methods to async def.@pytest.fixture on a fixture that returns something created via async def → fixture itself
becomes async def (pytest-asyncio with asyncio_mode = "auto" handles the await).@mock.patch(...) decorators: update string targets per §3b.unittest.TestCase inheritance when porting — project uses top-level async test
functions, not test classes.Replace in both import statements AND mock.patch("...") / mocker.patch("...") strings:
| botocore symbol | aiobotocore equivalent |
|---|---|
from botocore.session import Session | from aiobotocore.session import AioSession |
from botocore.credentials import CredentialResolver | from aiobotocore.credentials import AioCredentialResolver |
from botocore.args import ClientArgsCreator | from aiobotocore.args import AioClientArgsCreator |
from botocore.client import ClientCreator | from aiobotocore.client import AioClientCreator |
from botocore.signers import ... | from aiobotocore.signers import ... (no Aio prefix for module-level functions) |
Rule: look up the botocore class name in the aio_classes registry from registries.py. If
AioX exists, replace X with AioX in both imports and patch strings.
Inside each ported test body:
async_methods registry gets awaited:
session.create_client(...) → await session.create_client(...).async with: with session.create_client(...) →
async with session.create_client(...). For ExitStack / contextlib.ExitStack, use
contextlib.AsyncExitStack and enter_async_context.for chunk in stream → async for chunk in stream when stream is a response body.stream.read() → await stream.read() (read is in async_methods).client.get_paginator(...).paginate(...) returns an async iterator — use
async for page in paginator.paginate(...), not .build_full_result() which isn't
directly available. See aiobotocore/paginate.py::AioPageIterator for what's supported.mock.Mock(return_value=...) on a coroutine target → mock.AsyncMock(return_value=...).patch.object(obj, 'method', return_value=X) when method is in async_methods: use
AsyncMock(return_value=X) or new_callable=mock.AsyncMock.side_effect=[a, b, c] on async mocks: AsyncMock handles this natively; no change needed.side_effect=ExceptionClass on async mocks: AsyncMock also handles this natively.yield pattern, convert to
async def + add async with for the resource. Pytest-asyncio handles await on yields.asyncio_mode = "auto" is set in
pyproject.toml — rely on it. Any loop = asyncio.new_event_loop() / loop.run_until_complete
patterns must be rewritten: the test becomes async def, the inner awaitables just get await.time.sleep(X) in a test: replace with await asyncio.sleep(X) ONLY if the purpose is to
trigger time-sensitive behavior (e.g. credential refresh). For pure wait-until-state,
prefer polling with asyncio.wait_for.botocore.stub.Stubber and botocore.client.ClientHTTPStubber aren't directly compatible with
aiobotocore clients. Replace with the helpers already exported by tests.botocore_tests:
ClientHTTPStubber → from tests.botocore_tests import ClientHTTPStubber (aiobotocore's
version, already async-aware). Usage is identical: async with ClientHTTPStubber(client) as s:.SessionHTTPStubber → same import path.botocore.stub.Stubber for low-level API-level stubbing (not HTTP-level): no direct
equivalent. Flag the test in the advisory output with the note
"API-level Stubber — port manually; consider rewriting as ClientHTTPStubber with canned
HTTP responses, or move the test to the aiobotocore-specific test dir."botocore tests sometimes use responses (for requests-based mocking) or raw mock.patch
on HTTP internals. aiobotocore uses aiohttp and httpx — neither is compatible with responses.
responses.add(...) / @responses.activate → replace with ClientHTTPStubber (above) OR
if the test needs specific HTTP-layer behavior not expressible via Stubber, use
aioresponses (for aiohttp) / pytest-httpx (for httpx). Flag for human review if
unclear which backend the test needs.mock.patch('botocore.httpsession.URLLib3Session.send') → no direct equivalent in
aiobotocore (the async equivalent is AioHTTPSession.send). Usually the right move is to
use ClientHTTPStubber instead of patching the session class.threading / multiprocessing for parallelism — async equivalents use
asyncio.gather / asyncio.create_task. Human judgment needed on whether the test
exercises something aiobotocore actually does differently._endpoint, _auth). Don't blindly substitute.@pytest.mark.parametrize over sync/async variants — usually port just the
async variant; human picks if the sync case has value.botocore.tests.BaseSessionTest or similar class hierarchies. Convert the
TestCase → top-level async functions pattern; the fixture conversion requires judgment.For each successfully converted file:
tests/botocore_tests/<unit|functional>/<same-name>. If the mirror
file already has ported tests, APPEND the new tests at a sensible location (typically end of
file) rather than overwriting — preserve existing work.uv run pytest -xvs tests/botocore_tests/<path> on just that file. -x stops at the
first failure; -v surfaces the failing test name; -s shows prints for debugging.mcp__github_file_ops__commit_files
(the caller handles the actual commit batching).git checkout -- <path> restores it. If we created a new mirror
file (case 2 in Step 1), delete it with rm tests/botocore_tests/<unit|functional>/<name>.py
— there's no prior content to restore. (The allowed-tools entry scopes rm to
tests/botocore_tests/ only; any other path requires human approval. Respect that.)
Emit an advisory block describing which test(s) failed and which conversion rules were
applied; the human reviewer can pick up from there.Emit a structured report:
## Port-tests report (mode: <port-pr|backfill>)
### Successfully ported
- tests/botocore_tests/unit/test_credentials.py
- test_assume_role_refresh (new in 1.42.89)
- test_sso_token_fetch (new in 1.42.89)
- pytest: PASSED (2 new, 14 existing)
### Skipped (out of scope)
- botocore/tests/unit/test_awsrequest.py
- Reason: no existing aiobotocore mirror, and the new tests don't reference any
symbol in `overrides` / `aio_classes` — nothing for aiobotocore to exercise.
### Failed — needs human review
- tests/botocore_tests/unit/test_signers.py
- test_presigned_url_with_sigv4a: AssertionError on URL comparison
- Conversion notes: converted `client.generate_presigned_url` to `await`; patched
`aiobotocore.signers.generate_presigned_url`. Reverted write.
Never claim a test passes without actually running pytest on the ported file. If the botocore
clone isn't available or a tag is missing, return error: <reason> instead of attempting the
port. A falsely green port creates technical debt that's worse than not porting at all.
Sync bot (botocore-sync-prompt.md Step 5, port path): after bump-version lands the
version changes, invoke /aiobotocore-bot:port-tests --from=$FROM --to=$TO. Include the
resulting report in the port PR's "What changed in aiobotocore" section.
Humans: run /aiobotocore-bot:port-tests --backfill --paths=test_foo.py,test_bar.py to
narrow a coverage gap on specific files. The --dry-run flag is recommended first.