Fix bugs found in deep code review (v7.3.0) - #37
Merged
Conversation
Correctness fixes:
- dynamodb: upsert_item built an invalid UpdateExpression for multi-attribute
items (missing commas) and failed on reserved words; now uses
ExpressionAttributeNames/Values placeholders. Raise (not just construct)
AWSimpleException for item=None (same fix in cache.py eviction).
- s3: upload()/upload_object_as_json() compared against get_sha512(), which
never returns None (it synthesizes a substitute hash), so non-awsimple
objects always re-uploaded and the mtime fallback was dead code; compare
the raw .sha512 field instead. upload/download now raise AWSimpleException
after exhausting retries instead of silently returning False, so
download_cached no longer caches a failed download as success.
download() converts str dest_path instead of asserting on it.
- dynamodb: scan_table no longer swallows connection errors mid-pagination
(returned partial results as if complete, poisoning the pickle cache).
- dynamodb_miv: put_item now updates the metadata-table mtime (cache
invalidation was bypassed) and uses a conditional put with retries so
concurrent writers can't silently overwrite the same miv.
- pubsub: queue names now use the channel as a prefix so remove_old_queues()
can actually find them (previously a hash mismatch made cleanup a no-op);
make_name_aws_safe joins args with a separator so ("a","b") no longer
collides with ("ab",). Main loop drains the pub/sub queues each cycle
(was ~1 message per 10s under burst). Threads log-and-survive transient
AWS/JSON errors instead of dying silently; use getLogger instead of a
raw Logger instance.
- sqs/aws: boto_error_to_string no longer crashes on BotoCoreError subclasses
(HTTPClientError has no .response). get_all_sqs_queues passes credentials
through. add_permission and _connect_sns_to_sqs merge into the existing
queue policy instead of overwriting it.
- aws: moto mock lifecycle is now reference counted process-wide (env vars
restored correctly regardless of GC order). test() calls STS
GetCallerIdentity (get_available_resources was offline-only and raised
PermissionError for services without boto3 resources, e.g. logs).
LocalStack + resource_name=None no longer leaves .resource unset.
- dynamodb/sns: replace lru_cache/cache on instance methods (leaked every
instance for the process lifetime) with per-instance caches.
- dynamodb: metadata-table mtime update auto-creates the metadata table as
documented; delete_item/upsert_item/delete_all_items guard a None
metadata_table; scan_table_cached only unpickles on an actual cache hit
and stamps the cache file with time.time() so Windows clock-tick lag
can't make it appear older than the metadata mtime.
- s3: get_s3_object_url handles us-east-1 (LocationConstraint None) and uses
the current s3.{region} endpoint style; bucket location cached per
instance. get_s3_object_metadata uses a single head_object call (dir()
previously made ~3 API calls per object). bucket_exists uses the
configured session/localstack endpoint instead of a fresh default client.
- logs: on InvalidSequenceTokenException retry once with the expected token
instead of dropping the message; fallback file opens in append mode.
- dynamodb: dict_to_dynamodb keeps zero-length strings (supported by
DynamoDB since May 2020); create_table key types are str/int/bytes and
bool is rejected ("B" is Binary, not boolean).
- cache: LRU eviction skips directories; free-disk check measures the cache
directory's volume; SQS response history uses a JSON-safe sentinel key and
guards against an empty history.
- exceptions: single root exception (AWSimpleException) for all awsimple
errors; mock.py no longer freezes env-var reads with @cache.
Tests: conftest holds a session-scoped AWSAccess so moto state persists
across the suite (previously an accident of the lru_cache leak);
test_pubsub_list_queues creates its own queue; updated assertions that
codified the empty-string and name-collision bugs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_awsimple The STS-based test() raised a raw ClientError (e.g. InvalidClientTokenId) for invalid credentials, which broke examples/aws_access_test.py - notably under bare pytest, which collected examples/*_test.py and executed the example at import time. test() now returns False for ClientError / NoCredentialsError (setting most_recent_error) while configuration errors like ProfileNotFound still raise, and pytest is scoped to test_awsimple via pyproject testpaths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes ~30 issues found in a deep code review of the whole library, spanning correctness bugs, error handling, concurrency, and resource lifecycle. Full test suite passes (91/91, moto mock) and mypy is clean.
Correctness
upsert_itembuilt an invalidUpdateExpressionfor multi-attribute items (missing commas) and failed on DynamoDB reserved words; now usesExpressionAttributeNames/Valuesplaceholders. Exceptions that were constructed but never raised (upsert_item(item=None), cache eviction) are now raised.scan_tableno longer returns partial results on connection errors. Metadata-table mtime updates auto-create the metadata table (as documented) and are guarded consistently.get_sha512(), which never returnsNone, so non-awsimple objects always re-uploaded and the documented mtime fallback was dead code; now compares the raw.sha512.upload/downloadraiseAWSimpleExceptionafter exhausting retries instead of silently returningFalse(sodownload_cachedcan no longer cache a failed download as success).downloadacceptsstrpaths as documented.get_s3_object_urlhandles us-east-1 and uses the moderns3.{region}endpoint.bucket_existshonors the configured session/profile/LocalStack endpoint.get_s3_object_metadatauses a singlehead_object(dir() was ~3 API calls per object).put_itembypassed metadata mtime updates (stalescan_table_cachedresults) and could silently overwrite items under concurrent writes; now updates metadata and uses a conditional put with retries.make_name_aws_safejoins args with a separator so("a","b")no longer collides with("ab",). Main loop drains message queues each cycle (bursts previously throttled to ~1 message per 10 s). Threads survive transient AWS/JSON errors and log fatal failures instead of dying silently; module usesgetLogger.boto_error_to_stringno longer crashes onBotoCoreErrorsubclasses (the long-poll error handler crashed on the exact errors it was written to survive). Queue policies are merged rather than overwritten (subscribing to a second SNS topic no longer revokes the first).get_all_sqs_queuespasses credentials through.test()calls STSGetCallerIdentity(the old check was offline-only, validated nothing, and raisedPermissionErrorfor services like logs); returnsFalseon invalid credentials. LocalStack +resource_name=Noneno longer leaves.resourceunset.Resource lifecycle / API polish
functools.lru_cache/cacheon instance methods (dynamodb, sns) pinned every instance for the process lifetime; replaced with per-instance caches.AWSimpleException.dict_to_dynamodbpreserves zero-length strings (supported by DynamoDB since May 2020).create_tablekey types arestr/int/bytes;boolis rejected (DynamoDB "B" is binary).Behavior changes to note
upload()/download()raiseAWSimpleExceptionon persistent failure instead of returningFalse.test()returnsFalsefor invalid credentials instead of passing an offline check.remove_old_queues.dict_to_dynamodb("")returns""instead ofNone.Tests
AWSAccessso moto state persists across the suite deliberately (previously an accident of the lru_cache leak).scan_table_cachedstamps its cache file withtime.time(), fixing a Windows clock-granularity flake with only a ~5 ms natural margin.test_awsimpleviapyproject.tomlso barepytestdoesn't executeexamples/*_test.pyat import time.🤖 Generated with Claude Code