Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions datadog_checks_dev/changelog.d/21898.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Validate that dependencies are in the correct section in pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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!")
36 changes: 29 additions & 7 deletions datadog_checks_dev/datadog_checks/dev/tooling/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand Down
107 changes: 107 additions & 0 deletions ddev/tests/cli/validate/test_dep.py
Original file line number Diff line number Diff line change
@@ -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}"
Loading