diff --git a/datadog_checks_dev/changelog.d/21898.fixed b/datadog_checks_dev/changelog.d/21898.fixed new file mode 100644 index 0000000000000..83fe87b120070 --- /dev/null +++ b/datadog_checks_dev/changelog.d/21898.fixed @@ -0,0 +1 @@ +Validate that dependencies are in the correct section in pyproject.toml diff --git a/datadog_checks_dev/datadog_checks/dev/tooling/commands/validate/dep.py b/datadog_checks_dev/datadog_checks/dev/tooling/commands/validate/dep.py index f66267dcbd36b..c44c36a4da4b8 100644 --- a/datadog_checks_dev/datadog_checks/dev/tooling/commands/validate/dep.py +++ b/datadog_checks_dev/datadog_checks/dev/tooling/commands/validate/dep.py @@ -220,9 +220,9 @@ def dep(check, require_base_check_version, min_base_check_version): check_base_dependencies, check_base_errors = read_check_base_dependencies(check_name) annotate_errors(base_req_source, check_base_errors) if check_base_errors: + failed = True for check_error in check_base_errors: echo_failure(check_error) - abort() for name, versions in sorted(check_dependencies.items()): if not verify_dependency('Checks', name, versions, req_source): @@ -279,6 +279,6 @@ def dep(check, require_base_check_version, min_base_check_version): annotate_error(agent_dependencies_file, message) continue - if failed: - abort() + if failed: + abort() echo_success("All dependencies are valid!") diff --git a/datadog_checks_dev/datadog_checks/dev/tooling/dependencies.py b/datadog_checks_dev/datadog_checks/dev/tooling/dependencies.py index 8d5aa41decbcb..185711ff2292a 100644 --- a/datadog_checks_dev/datadog_checks/dev/tooling/dependencies.py +++ b/datadog_checks_dev/datadog_checks/dev/tooling/dependencies.py @@ -97,19 +97,32 @@ def load_dependency_data_from_requirements(req_file, dependencies, errors, check def load_base_check(check_name, dependencies, errors): project_data = load_project_file_cached(check_name) check_dependencies = project_data['project'].get('dependencies', []) + dependency_errors = [] + found_base = False + for check_dependency in check_dependencies: try: req = Requirement(check_dependency) except InvalidRequirement as e: - errors.append(f'File `{check_name}/pyproject.toml` has an invalid dependency: `{check_dependency}`\n{e}') + errors.append(f'There is an invalid dependency: `{check_dependency}`\n{e}') continue name = normalize_project_name(req.name) if name == 'datadog-checks-base': dependencies[check_name] = get_normalized_dependency(req) - break - else: - errors.append(f'File `{check_name}/pyproject.toml` is missing the base check dependency `datadog-checks-base`') + found_base = True + else: + dependency_errors.append(name) + + if not found_base: + errors.append('Missing the required base check dependency `datadog-checks-base` in project.dependencies') + + if dependency_errors: + dependency_errors_str = ', '.join(f'`{error}`' for error in dependency_errors) + errors.append( + f'Found third-party dependencies in project.dependencies: {dependency_errors_str}. ' + '\n - Third-party dependencies belong in project.optional-dependencies' + ) def load_base_check_legacy(req_file, dependencies, errors, check_name=None): @@ -120,14 +133,14 @@ def load_base_check_legacy(req_file, dependencies, errors, check_name=None): dep = line.split(' = ')[1] req = Requirement(dep.strip("'\"")) except (IndexError, InvalidRequirement) as e: - errors.append(f'File `{req_file}` has an invalid base check dependency: `{line}`\n{e}') + errors.append(f'Has an invalid base check dependency: `{line}`\n{e}') return dependencies[check_name] = get_normalized_dependency(req) return # no `CHECKS_BASE_REQ` found in setup.py file .. - errors.append(f'File `{req_file}` missing base check dependency `CHECKS_BASE_REQ`') + errors.append('Missing base check dependency `CHECKS_BASE_REQ`') def read_check_dependencies(check=None): @@ -157,6 +170,7 @@ def read_check_base_dependencies(check=None): root = get_root() dependencies = {} errors = [] + error_msg = [] if isinstance(check, list): checks = sorted(check) @@ -171,11 +185,19 @@ def read_check_base_dependencies(check=None): if has_project_file(check_name): load_base_check(check_name, dependencies, errors) + if errors: + file_name = f"{check_name}/pyproject.toml" else: req_file = os.path.join(root, check_name, 'setup.py') load_base_check_legacy(req_file, dependencies, errors, check_name) + if errors: + file_name = req_file - return dependencies, errors + if errors: + errors_str = '\n'.join(f' - {error}' for error in errors) + error_msg = [f'\n{file_name} has the following errors:\n{errors_str}'] + + return dependencies, error_msg def update_check_dependencies_at(path, dependencies): diff --git a/ddev/tests/cli/validate/test_dep.py b/ddev/tests/cli/validate/test_dep.py new file mode 100644 index 0000000000000..a4a223b5baed8 --- /dev/null +++ b/ddev/tests/cli/validate/test_dep.py @@ -0,0 +1,107 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +import re + +from tests.helpers.api import write_file + +error_regex = re.compile(r"(?s)^\s*[A-Za-z0-9_\/.-]+\.toml has the following errors:\n(?: - .+\n)+") +match_regex = re.compile(r"^\s*All dependencies are valid!\s*$") + + +def test_valid_integration(fake_repo, ddev): + write_file( + fake_repo.path / 'valid_check', + 'pyproject.toml', + """ + [project] + dependencies = [ + "datadog-checks-base>=37.21.0", + ] + """, + ) + result = ddev('validate', 'dep', 'valid_check') + assert result.exit_code == 0 + assert match_regex.match(result.output), f"Unexpected output: {result.output}" + + +def test_invalid_third_party_integration(fake_repo, ddev): + write_file( + fake_repo.path / 'bad_check', + 'pyproject.toml', + """ + [project] + dependencies = [ + "datadog-checks-base>=37.21.0", + "dep-d==1.5.0", + ] + """, + ) + result = ddev('validate', 'dep', 'bad_check') + assert result.exit_code == 1 + assert error_regex.search(result.output), f"Unexpected output: {result.output}" + + +def test_multiple_invalid_third_party_integrations(fake_repo, ddev): + write_file( + fake_repo.path / 'bad_check_2', + 'pyproject.toml', + """ + [project] + dependencies = [ + "dep-b==1.5.0", + "dep-e==1.5.0", + ] + """, + ) + + write_file( + fake_repo.path / 'bad_check_3', + 'pyproject.toml', + """ + [project] + dependencies = [ + "datadog-checks-base>=37.21.0", + "dep-f==1.5.0", + ] + """, + ) + + result = ddev('validate', 'dep', 'bad_check_2') + result_2 = ddev('validate', 'dep', 'bad_check_3') + assert result.exit_code == 1 + assert error_regex.search(result.output), f"Unexpected output: {result.output}" + assert result_2.exit_code == 1 + assert error_regex.search(result_2.output), f"Unexpected output: {result_2.output}" + + +def test_one_valid_one_invalid_integration(fake_repo, ddev): + write_file( + fake_repo.path / 'valid_check_2', + 'pyproject.toml', + """ + [project] + dependencies = [ + "datadog-checks-base>=37.21.0", + ] + """, + ) + + write_file( + fake_repo.path / 'bad_check_4', + 'pyproject.toml', + """ + [project] + dependencies = [ + "datadog-checks-base>=37.21.0", + "dep-f==1.5.0", + ] + """, + ) + + result = ddev('validate', 'dep', 'valid_check_2') + result_2 = ddev('validate', 'dep', 'bad_check_4') + assert result.exit_code == 0 + assert match_regex.match(result.output), f"Unexpected output: {result.output}" + assert result_2.exit_code == 1 + assert error_regex.search(result_2.output), f"Unexpected output: {result_2.output}"