BATCH-3679: gate S3 default client on IS_LUIGI1_DEPRECATED flag#30
Open
andresvelasquez2-affirm wants to merge 4 commits into
Conversation
Add a private luigi.contrib._luigi1_compat module exposing IS_LUIGI1_DEPRECATED (True when `import luigi1` raises, False otherwise). luigi.contrib.s3 now binds the public S3Client and ReadableS3File aliases through this flag, so legacy environments default to the boto1 stack and modern environments default to boto3 without any per-caller change. S3ClientBoto1 / S3ClientBoto3 remain public for callers that need to pin a stack via client=. Update s3_test.py to be flag-aware (USING_BOTO1, BOTO1_RUNNABLE, S3CLIENT_INSTANTIABLE) and add TestFlagDrivenS3Dispatch plus test/contrib/luigi1_compat_test.py covering the dispatch contract. Co-Authored-By: Claude Opus 4.7 (1M context) <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.
Description
Adds an environment-driven switch that picks the default S3 client implementation based on whether
luigi1is still installed alongside this fork.luigi/contrib/_luigi1_compat.pyexposing one symbol —IS_LUIGI1_DEPRECATED:Falsewhenimport luigi1succeeds (legacy stack still around)Truewhenimport luigi1raises anyException(legacy stack gone)luigi/contrib/s3.pynow binds the public aliases through this flag:IS_LUIGI1_DEPRECATED is True→S3Client = S3ClientBoto3,ReadableS3File = ReadableS3FileBoto3IS_LUIGI1_DEPRECATED is False→S3Client = S3ClientBoto1,ReadableS3File = ReadableS3FileBoto1S3ClientBoto1/S3ClientBoto3(and the matchingReadableS3File*classes) remain public, so callers that need to pin a stack regardless of environment continue to do so viaclient=.client=kwargs onS3Target,S3PathTask,S3EmrTask,S3FlagTaskare unchanged. The flag covers the default dispatch; the kwarg covers explicit injection (custom credentials, assumed roles, moto-mocked clients,RedshiftManifestTask'sself._clientforwarding).setup.py.CLAUDE.mdandPLAN_Py39_P312_DUAL_COMPATIBILITY.mdupdated with the rationale, the surface controlled by the flag, and the test infra changes.Motivation and Context
BATCH-3679. The base branch (
rafay/BATCH-3679-luigi-py-312-upgrade) ships both boto1 and boto3 stacks side-by-side and pins the default to boto1 for backwards compatibility. That forces every caller in a modern environment (whereluigi1has already been removed) to migrate explicitly toclient=S3ClientBoto3()before they can dropluigi1.This PR makes the cutover environmental instead of per-call-site: while
luigi1is installed the legacy boto1 default is preserved verbatim, and the dayluigi1is uninstalled the same call sites land on boto3 with no code change. Existingclient=overrides continue to win when callers need to pin a specific stack.Have you tested this? If so, how?
I have included unit tests, and I have run the s3 contrib tests across 7 environment combinations.
Unit tests:
test/contrib/luigi1_compat_test.py— new file. Covers all three branches ofIS_LUIGI1_DEPRECATED: importable (False), unimportable (True viaImportError), and importable-but-raises-non-ImportError(True). Uses asys.meta_pathfinder to actively block / injectluigi1so the test is deterministic regardless of whetherluigi1is genuinely installed in the env.test/contrib/s3_test.py::TestFlagDrivenS3Dispatch— new test class. Pins the dispatch contract: alias resolution per flag,_readable_file_clswiring, default client injection inS3Target/S3PathTask/S3EmrTask/S3FlagTask, same-stack and cross-stackclient=override, and (for the harder reload case) the alias flip in both directions. Two simple, hermetic tests pin the literal class identity ofS3Target('s3://...').fsper flag value, gated on the natural flag state — coverage of both directions comes from running under differentuvscenarios:test_s3_target_default_fs_is_boto3_when_flag_true—@unittest.skipUnless(IS_LUIGI1_DEPRECATED, ...). Assertstarget.fsis anS3ClientBoto3(and notS3ClientBoto1). Exercised in scenarios 2/3/4/5.test_s3_target_default_fs_is_boto1_when_flag_false—@unittest.skipUnless(not IS_LUIGI1_DEPRECATED and HAS_BOTO_PKG, ...). Assertstarget.fsis anS3ClientBoto1(and notS3ClientBoto3). Exercised in scenario 6.Test-infra refinements made along the way:
try: import boto / HAS_BOTOsetup was replaced with flag-aware predicates:USING_BOTO1,HAS_BOTO_PKG,BOTO1_RUNNABLE = USING_BOTO1 and HAS_BOTO_PKG,S3CLIENT_INSTANTIABLE = HAS_BOTO_PKG if USING_BOTO1 else True. Class-level skips onTestS3Target/TestS3Client(and per-test on the dispatch tests that constructS3Client()) ensure degenerate configs (flag selects boto1 butbotomissing) skip cleanly instead of failing at import or instantiation.S3ResponseErroris now bound to the active stack, not just package availability — boto can be on path while the flag selects boto3, and in that case errors come frombotocore.mock_s3/mock_stsselection is now stack-aware. Under the boto1 stack we prefer moto 1.x's HTTPretty-backedmock_s3_deprecated/mock_sts_deprecated, which patch at the socket level and therefore catch boto1's rawhttp.clienttraffic. The defaultmock_s3only patchesrequests/urllib3and silently lets boto1 traffic reach real AWS; that's why the boto1 path looked "broken" before this fix even though the test code was fine. moto 4+ dropped support for boto1 mocking entirely.test_init_with_config_and_roleshad a stale assertion expectings3_client.s3.access_key == 'AKIAIOSFODNN7EXAMPLE'— moto's STS mock now (correctly) returns AWS-canonicalASIA…-prefixed temporary credentials. Loosened to assert theASIASTS prefix and a non-empty secret key, which is what the test was actually meant to verify.Scenario matrix (
uv run --no-project --index-url https://pypi.affirm-build.com/artifactory/api/pypi/pypi/simple/ --with-editable . --with pytest --with mock --with requests <stack-deps> python -m pytest -q test/contrib/s3_test.py test/contrib/luigi1_compat_test.py):botois missing); flag dispatch contract still verified by the 8 non-instantiating testsEvery viable scenario is green. Scenario 1 is the only failure and it's the predicted "boto1 isn't py3.12 compatible" outcome — there's no way to run that combination because the legacy stack itself doesn't load on py3.12. Both flag states are exercised: scenarios 2/3/4/5 cover flag-True (boto3 default), scenario 6 covers flag-False (boto1 default with the proper HTTPretty-backed moto mock).