Check GCSToS3Operator match_glob support after template rendering - #70723
Check GCSToS3Operator match_glob support after template rendering#70723atikulmunna wants to merge 4 commits into
Conversation
`match_glob` is a template field, so it is rendered after the constructor runs. The support check in `__init__` therefore tested the un-rendered Jinja expression for truthiness: a templated `match_glob` was rejected at Dag-parse time regardless of what it rendered to, and the error surfaced as a Dag import error rather than on the task. Move the check to `execute()`, onto the `elif` branch of the existing `__is_match_glob_supported` test, which is where the rendered value would otherwise be forwarded to `GCSHook.list()`. This mirrors the same change made for the sibling `GCSToAzureBlobStorageOperator` in gcs_to_wasb.py. Add a test for the error, which was previously uncovered, and remove the class from the validate-operators-init exemption list. related: apache#70296 Signed-off-by: atikulmunna <atikul.munna@northsouth.edu>
|
Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
|
potiuk
left a comment
There was a problem hiding this comment.
Thanks — correct fix. match_glob is in template_fields, so validating it in __init__ meant a templated value was checked before rendering, and a failure there breaks Dag parsing rather than failing the task. Moving it into execute alongside the _is_match_glob_supported branch is the right place, and the version probe itself correctly stays in __init__ since it depends on the installed provider, not on the rendered value. Dropping the entry from validate_operators_init_exemptions.txt completes it.
Test is good — asserting list.assert_not_called() proves it fails before doing any work.
One consistency point inline.
Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting
potiuk
left a comment
There was a problem hiding this comment.
Retracting my earlier approval — my mistake, and I'm sorry for the churn.
I approved this before reading #70296, which sets out the rule for this burn-down, and by that rule this change moves the wrong kind of check.
A check that only asks whether an argument was passed … belongs in
__init__and must not be moved. Fix these by rewriting in place, not by moving. Use theis not Nonepolarity.
The reasoning in the issue is sound and I'd missed both halves of it:
- With
render_template_as_native_obj=True, a field that was provided can render toNone, so the same check inexecute()reports a supplied argument as missing. - Raising in the constructor surfaces a static authoring mistake as a Dag import error, rather than once per task instance and per retry on a worker.
if not self.__is_match_glob_supported and self.match_glob: asks whether match_glob was passed (combined with an environment capability that __init__ can already answer). It's a provision check, so it should be rewritten in place rather than relocated:
# in __init__ — keep it here
if not self.__is_match_glob_supported and match_glob is not None:
raise ValueError(
"The 'match_glob' parameter requires 'apache-airflow-providers-google>=10.3.0'."
)Note the is not None polarity matters: if match_glob: is a truthiness test on the un-rendered Jinja string, which is a third question that matches neither intent.
My earlier point about narrowing AirflowException to ValueError still stands and is worth doing in the same edit — #70503 makes the same call for dms.py.
Good news on mechanics: #70505 (which narrows the hook to allow provision checks written with is not None) merged on 28 July, so the rewrite below passes validate-operators-init and you can still remove the exemption-file entry in this PR — the burn-down goal is unaffected.
There's an active follow-up, #70503, cataloguing already-merged PRs that made exactly this move so they can be put back. Fixing it here saves this PR from joining that list.
Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting
Addresses review feedback on apache#70723. The check asks whether `match_glob` was passed rather than inspecting its rendered value, so per the false positives section of apache#70296 it is a provision check and belongs in the constructor. Moving it to `execute()` was wrong on two counts: with `render_template_as_native_obj=True` a provided field can render to None, and raising per task instance hides a static authoring mistake that a Dag import error would surface immediately. Rewrite it in place instead, using `is not None` polarity rather than a truthiness test on the un-rendered string, and narrow `AirflowException` to `ValueError` to match apache#70359. Since apache#70505 narrowed the hook to sanction provision checks written this way, the exemption entry still goes. Narrowing removes the file's only `raise AirflowException`, so drop its entry from generated/known_airflow_exceptions.txt to keep the check-no-new-airflow-exceptions allowlist in sync. related: apache#70296 Signed-off-by: atikulmunna <atikul.munna@northsouth.edu>
|
Thanks for the correction, and no need to apologise for the churn. You're right. I mirrored #70574 and treated it as settled precedent instead of checking it against the false positives section of #70296, which is exactly the failure mode you describe in your sweep. Rewritten in place:
One thing that wasn't in your review: narrowing to Also worth correcting my own PR description: I said there I couldn't run the tests. I've since got them running in a Linux container and the file is green, 31 passed. The Windows blocker was The version probe stays in |
shahar1
left a comment
There was a problem hiding this comment.
The rewrite lands where #70296 says it should, and it matches the #70359
precedent one-for-one. Two small things below, neither blocking — I'm leaving
this as a comment rather than an approval only because the earlier
CHANGES_REQUESTED is still the standing review state.
Things I checked rather than assumed:
match_glob is not Noneis the shape the hook sanctions —
_collect_sanctioned_uses()inscripts/ci/prek/validate_operators_init.py
explicitly whitelistsis None/is not Nonecomparisons on template
fields, which the old truthiness test was not. Removing the
validate_operators_init_exemptions.txtentry is correct.- The
generated/known_airflow_exceptions.txtdeletion is what the generator
itself would produce:AllowlistManager.check()doesdel allowlist[rel]
rather than tightening to::0when a file's count reaches zero. Keeping the
unrelatedmlengine.pyentry out of the diff was the right call — that file
no longer exists, and cleaning it up belongs in its own PR. - Shape matches
f781f8b878(#70359,S3DeleteObjectsOperator) exactly: same
four files, same polarity, sameValueErrornarrowing, no changelog entry.
Perproviders/AGENTS.md§ "Changelog — never use newsfragments", nothing
further is needed.
Smaller observations
-
PR description no longer matches the diff. It still describes the first
approach — check removed from__init__, raised fromexecute(),
AirflowExceptiondeliberately kept, test named
test_execute__match_glob_requires_recent_google_provider. All four are now
false; the second commit reversed them. It also still states the unit tests
couldn't be run locally, which you corrected in a comment. The review
criteria call this out directly:Description doesn't match code: PR description describes something
different from what the code actually does.—
.github/instructions/code-review.instructions.md.
The commit messages are accurate, so this is only the body; worth a quick
rewrite so the next reader isn't chasing anexecute()change that isn't
there. -
providers/amazon/tests/unit/amazon/aws/transfers/test_gcs_to_s3.py:79— the
new test doesn't lock in theis not Nonepolarity; see the inline comment.
This review was drafted by an AI-assisted tool and
confirmed by an Airflow maintainer. The findings
below are observations, not blockers; an Airflow
maintainer — a real person — will take the next look at the
PR. If you think a finding is mis-applied, please reply on
the PR and a maintainer will weigh in.More on how Airflow handles maintainer review:
contributing-docs/05_pull_requests.rst.
Drafted-by: Claude Code (Opus 5); reviewed by @shahar1 before posting
| @mock.patch("airflow.providers.google.__version__", "10.2.0") | ||
| def test_match_glob_requires_recent_google_provider(self): | ||
| with pytest.raises(ValueError, match="match_glob"): | ||
| GCSToS3Operator( | ||
| task_id=TASK_ID, | ||
| gcs_bucket=GCS_BUCKET, | ||
| prefix=PREFIX, | ||
| dest_aws_conn_id="aws_default", | ||
| dest_s3_key=S3_BUCKET, | ||
| match_glob=f"**/*{DELIMITER}", | ||
| ) |
There was a problem hiding this comment.
The test passes match_glob=f"**/*{DELIMITER}", which is truthy, so it would still pass against the old if not ... and match_glob: condition — the only thing failing it on main is the AirflowException → ValueError swap, not the polarity change. Since is not None vs. truthiness was the substance of the review round, worth parametrising so the empty-string case is pinned:
| @mock.patch("airflow.providers.google.__version__", "10.2.0") | |
| def test_match_glob_requires_recent_google_provider(self): | |
| with pytest.raises(ValueError, match="match_glob"): | |
| GCSToS3Operator( | |
| task_id=TASK_ID, | |
| gcs_bucket=GCS_BUCKET, | |
| prefix=PREFIX, | |
| dest_aws_conn_id="aws_default", | |
| dest_s3_key=S3_BUCKET, | |
| match_glob=f"**/*{DELIMITER}", | |
| ) | |
| @mock.patch("airflow.providers.google.__version__", "10.2.0") | |
| @pytest.mark.parametrize("match_glob", [f"**/*{DELIMITER}", ""]) | |
| def test_match_glob_requires_recent_google_provider(self, match_glob): | |
| with pytest.raises(ValueError, match="match_glob"): | |
| GCSToS3Operator( | |
| task_id=TASK_ID, | |
| gcs_bucket=GCS_BUCKET, | |
| prefix=PREFIX, | |
| dest_aws_conn_id="aws_default", | |
| dest_s3_key=S3_BUCKET, | |
| match_glob=match_glob, | |
| ) |
AGENTS.md asks for @pytest.mark.parametrize when tests differ only in input, so this stays one test rather than two.
Addresses review feedback on apache#70723. The test only passed a truthy `match_glob`, so it would also have passed against the old `if not ... and match_glob:` condition; the only thing failing it on main was the `AirflowException` to `ValueError` swap, not the polarity change that was the substance of the review. Parametrise over a glob and the empty string so `is not None` is pinned: `match_glob=""` is rejected under the new condition and accepted under the old one, which is exactly the behaviour change this PR makes. related: apache#70296 Signed-off-by: atikulmunna <atikul.munna@northsouth.edu>
|
Both fixed in 3c8d638 plus a description rewrite. Test: you're right, it wasn't testing the polarity at all. The truthy glob would have passed the old condition too, so the Description: it was still describing the first revision, including the bit about not being able to run the tests locally. Rewritten to match the diff, with a note that the earlier |
…-glob-after-rendering Signed-off-by: atikulmunna <atikul.munna@northsouth.edu> # Conflicts: # scripts/ci/prek/validate_operators_init_exemptions.txt
Rewrites the
match_globsupport check inGCSToS3Operator.__init__so it reads whether the argument was passed, and narrows it toValueError.match_globis a template field, so the constructor only ever sees the un-rendered Jinja expression. The old check,if not self.__is_match_glob_supported and match_glob:, was a truthiness test on that string, which asks a third question matching neither intent.It stays in
__init__. It is a provision check, not a value check: it asks whethermatch_globwas supplied, combined with an environment capability the constructor can already answer. Per the false positives section of #70296 those belong in the constructor and get rewritten in place rather than moved, because withrender_template_as_native_obj=Truea provided field can render toNone, and raising at construction surfaces a static authoring mistake as a Dag import error instead of once per task instance and per retry.Since #70505 narrowed
validate-operators-initto sanctionis None/is not Nonereads, the rewrite passes the hook and the exemption entry still goes.This change:
match_glob is not None, which is strictly stricter than the truthiness test since it also rejectsmatch_glob=""AirflowExceptiontoValueError, matching Keep S3DeleteObjectsOperator validation in __init__, narrow to ValueError #70359, which also dropsAirflowExceptionfrom thecommon.compat.sdkimport as nothing else in the file used itgcs_to_s3.py::1entry fromgenerated/known_airflow_exceptions.txt, since narrowing removed its onlyraise AirflowExceptiontest_match_glob_requires_recent_google_provider, parametrised over a glob and the empty string so theis not Nonepolarity is pinned, where nothing covered this error beforeGCSToS3Operatorfromscripts/ci/prek/validate_operators_init_exemptions.txtAn earlier revision of this PR moved the check into
execute()instead, mirroring #70574. That was wrong for the reasons above, and @potiuk's review corrected it; the current diff is the in-place rewrite.Verified locally:
The test suite runs in a Linux container rather than natively;
_shared/observability/metrics/stats.pycallsos.register_at_forkat import, which is POSIX only.related: #70296
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code following the guidelines