-
Notifications
You must be signed in to change notification settings - Fork 17.5k
Added static check for conn-fields defined in provider.yaml #69473
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
dabla
merged 3 commits into
apache:main
from
dabla:feature/add-static-check-conn-fields-provider-yaml
Jul 9, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| #!/usr/bin/env python | ||
| # | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The ASF licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
| """ | ||
| Validation helpers for the conn-fields ↔ get_connection_form_widgets() check. | ||
|
|
||
| These functions have no third-party dependencies so they can be unit-tested | ||
| outside of the Breeze container without any stubbing. | ||
|
|
||
| Used by ``scripts/in_container/run_provider_yaml_files_check.py``. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Callable | ||
|
|
||
|
|
||
| def check_conn_fields_for_entry( | ||
| conn_type_entry: dict, | ||
| yaml_file_path: str, | ||
| get_widget_keys: Callable[[str], set[str] | None], | ||
| ) -> list[str]: | ||
| """ | ||
| Validate a single connection-type entry. Returns a (possibly empty) list of error strings. | ||
|
|
||
| *get_widget_keys(hook_class_name)* is a callable supplied by the caller that: | ||
|
|
||
| - returns the set of field keys from ``get_connection_form_widgets()`` on success, | ||
| - returns ``None`` to signal that the hook could not be imported, its UI | ||
| dependencies are absent, or it does not implement ``get_connection_form_widgets()`` | ||
| at all — the entry is then skipped entirely (no ``conn-fields`` check), or | ||
| - raises any other ``Exception`` to signal an unexpected failure (converted | ||
| here into an error string so callers never need to catch it). | ||
| """ | ||
| hook_class_name: str = conn_type_entry["hook-class-name"] | ||
| connection_type: str = conn_type_entry.get("connection-type", "?") | ||
|
|
||
| try: | ||
| widget_keys = get_widget_keys(hook_class_name) | ||
| except Exception as exc: | ||
| return [ | ||
| f"Failed to call `{hook_class_name}.get_connection_form_widgets()` " | ||
| f"while checking {yaml_file_path}: {exc}" | ||
| ] | ||
|
|
||
| if widget_keys is None: | ||
| return [] | ||
|
|
||
| conn_fields = conn_type_entry.get("conn-fields") | ||
| if conn_fields is None: | ||
| # No conn-fields declared: the new UI simply exposes no custom fields for this | ||
| # connection type, which is intentional. Nothing to validate. | ||
| return [] | ||
|
|
||
| error = build_mismatch_error( | ||
| set(conn_fields.keys()), widget_keys, connection_type, yaml_file_path, hook_class_name | ||
| ) | ||
| return [error] if error else [] | ||
|
|
||
|
|
||
| def build_mismatch_error( | ||
| yaml_keys: set[str], | ||
| hook_keys: set[str], | ||
| connection_type: str, | ||
| yaml_file_path: str, | ||
| hook_class_name: str, | ||
| ) -> str | None: | ||
| """ | ||
| Check that every key declared in ``conn-fields`` exists in | ||
| ``get_connection_form_widgets()``. | ||
|
|
||
| ``conn-fields`` is the new React UI's view of a connection type and is | ||
| intentionally a *subset* of the Flask form widgets — fields can be omitted | ||
| from ``conn-fields`` on purpose. We therefore only flag keys that appear in | ||
| ``conn-fields`` but are absent from the hook's form (invalid / stale | ||
| declarations). The reverse direction (hook fields not listed in | ||
| ``conn-fields``) is not an error. | ||
|
|
||
| Return an error string when stale keys are found, or ``None`` when the | ||
| declared keys are all valid. | ||
| """ | ||
| only_in_yaml = yaml_keys - hook_keys | ||
|
|
||
| if not only_in_yaml: | ||
| return None | ||
|
|
||
| lines = [ | ||
| f"Mismatch between `conn-fields` in {yaml_file_path} and " | ||
| f"`{hook_class_name}.get_connection_form_widgets()` " | ||
| f"for connection-type '{connection_type}':" | ||
| ] | ||
| lines.append( | ||
| " Fields in provider.yaml conn-fields but NOT in get_connection_form_widgets(): " | ||
| + ", ".join(sorted(only_in_yaml)) | ||
| ) | ||
| lines.append("[yellow]How to fix it[/]: Remove the stale key(s) from conn-fields in provider.yaml.") | ||
| return "\n".join(lines) |
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
124 changes: 124 additions & 0 deletions
124
scripts/tests/ci/prek/test_check_conn_fields_match_form_widgets.py
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The ASF licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
| from __future__ import annotations | ||
|
|
||
| import pytest | ||
| from check_provider_conn_fields import ( | ||
| build_mismatch_error, | ||
| check_conn_fields_for_entry, | ||
| ) | ||
|
|
||
| YAML_PATH = "providers/my_provider/provider.yaml" | ||
| HOOK_CLASS = "my_provider.hooks.my_hook.MyHook" | ||
| CONN_TYPE = "my_conn_type" | ||
|
|
||
|
|
||
| def _entry(conn_fields: list[str] | None) -> dict: | ||
| entry: dict = {"hook-class-name": HOOK_CLASS, "connection-type": CONN_TYPE} | ||
| if conn_fields is not None: | ||
| entry["conn-fields"] = {k: {} for k in conn_fields} | ||
| return entry | ||
|
|
||
|
|
||
| def _get_keys(*keys: str): | ||
| """Return a get_widget_keys callable that always returns the given keys.""" | ||
| return lambda _hook_class_name: set(keys) | ||
|
|
||
|
|
||
| def _skip(_hook_class_name: str) -> None: | ||
| """get_widget_keys callable that signals 'skip silently'.""" | ||
| return None | ||
|
|
||
|
|
||
| def _raise(_hook_class_name: str) -> None: | ||
| raise RuntimeError("boom") | ||
|
|
||
|
|
||
| class TestBuildMismatchError: | ||
| @pytest.mark.parametrize( | ||
| "yaml_keys, hook_keys", | ||
| [ | ||
| pytest.param({"a", "b"}, {"a", "b"}, id="matching-keys"), | ||
| pytest.param(set(), set(), id="empty-sets"), | ||
| # Hook may have more keys than conn-fields — that is intentional (subset allowed). | ||
| pytest.param({"a"}, {"a", "extra_hook"}, id="hook-has-extra-keys-no-error"), | ||
| ], | ||
| ) | ||
| def test_no_mismatch_returns_none(self, yaml_keys, hook_keys): | ||
| assert build_mismatch_error(yaml_keys, hook_keys, CONN_TYPE, YAML_PATH, HOOK_CLASS) is None | ||
|
|
||
| @pytest.mark.parametrize( | ||
| "yaml_keys, hook_keys, expected_in_error, not_expected_in_error", | ||
| [ | ||
| pytest.param( | ||
| {"a", "extra_yaml"}, | ||
| {"a"}, | ||
| ["extra_yaml", "NOT in get_connection_form_widgets"], | ||
| ["NOT in provider.yaml conn-fields"], | ||
| id="extra-in-yaml", | ||
| ), | ||
| # only_in_hook no longer triggers an error — only only_in_yaml does | ||
| pytest.param( | ||
| {"a", "only_yaml"}, | ||
| {"a", "only_hook"}, | ||
| [ | ||
| "only_yaml", | ||
| "NOT in get_connection_form_widgets", | ||
| ], | ||
| ["only_hook", "NOT in provider.yaml conn-fields"], | ||
| id="both-sides-only-yaml-reported", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_mismatch_error_content(self, yaml_keys, hook_keys, expected_in_error, not_expected_in_error): | ||
| error = build_mismatch_error(yaml_keys, hook_keys, CONN_TYPE, YAML_PATH, HOOK_CLASS) | ||
| assert error is not None | ||
| for expected in expected_in_error: | ||
| assert expected in error | ||
| for not_expected in not_expected_in_error: | ||
| assert not_expected not in error | ||
|
|
||
|
|
||
| class TestCheckConnFieldsForEntry: | ||
| @pytest.mark.parametrize( | ||
| "conn_fields, get_keys", | ||
| [ | ||
| pytest.param(["a", "b"], _get_keys("a", "b"), id="matching-keys"), | ||
| pytest.param([], _get_keys(), id="empty-on-both-sides"), | ||
| pytest.param(["a"], _skip, id="skip-hook-without-get-connection-form-widgets"), | ||
| pytest.param(None, _skip, id="skip-missing-conn-fields-when-hook-has-no-widgets"), | ||
| # Hook with widgets but no conn-fields is allowed: new UI intentionally omits custom fields. | ||
| pytest.param(None, _get_keys("field_a"), id="no-conn-fields-with-hook-widgets-is-ok"), | ||
| # Hook has extra keys not in conn-fields — allowed (conn-fields is a valid subset). | ||
| pytest.param(["a"], _get_keys("a", "extra_hook"), id="hook-extra-keys-no-error"), | ||
| ], | ||
| ) | ||
| def test_no_errors(self, conn_fields, get_keys): | ||
| assert check_conn_fields_for_entry(_entry(conn_fields), YAML_PATH, get_keys) == [] | ||
|
|
||
| @pytest.mark.parametrize( | ||
| "conn_fields, get_keys, expected_in_error", | ||
| [ | ||
| pytest.param(["a", "extra"], _get_keys("a"), "extra", id="extra-key-in-yaml"), | ||
| pytest.param(["a"], _raise, "boom", id="unexpected-exception-message"), | ||
| pytest.param(["a"], _raise, HOOK_CLASS, id="unexpected-exception-mentions-hook-class"), | ||
| ], | ||
| ) | ||
| def test_one_error_containing(self, conn_fields, get_keys, expected_in_error): | ||
| errors = check_conn_fields_for_entry(_entry(conn_fields), YAML_PATH, get_keys) | ||
| assert len(errors) == 1 | ||
| assert expected_in_error in errors[0] |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.