Skip to content

Commit

Permalink
Add support for dynamic dependencies
Browse files Browse the repository at this point in the history
The dynamic dependencies are part of pep621.
For now only the setuptools supports dynamic dependencies.
Because of that the implementation is somehow dependent on setuptools.
When other tool adds support for dynamic dependencies
only the part for gathering of the dependencies has to be changed.
  • Loading branch information
Mihail Andreev committed Apr 26, 2024
1 parent f7f0c97 commit da4310b
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 0 deletions.
23 changes: 23 additions & 0 deletions src/flake8_requirements/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,27 @@ def get_pyproject_toml_pep621(cls):
cfg_pep518 = cls.get_pyproject_toml()
return cfg_pep518.get('project', {})

@classmethod
def get_setuptools_dynamic_requirements(cls):
"""Retrieve dynamic requirements defined in setuptools configuration."""
cfg = cls.get_pyproject_toml()
dynamic_keys = cfg.get('project', {}).get('dynamic', [])
dynamic_config = cfg.get('tool', {}).get('setuptools', {}).get('dynamic', {})
requirements = []
files_to_parse = []
if 'dependencies' in dynamic_keys:
files_to_parse.extend(dynamic_config.get('dependencies', {}).get('file', []))
if 'optional-dependencies' in dynamic_keys:
for element in dynamic_config.get('optional-dependencies', {}).values():
files_to_parse.extend(element.get('file', []))
for file_path in files_to_parse:
try:
with open(file_path, 'r') as file:
requirements.extend(parse_requirements(file))
except (IOError) as e:
LOG.debug("Couldn't open requirements file: %s", e)
return requirements

@classmethod
def get_pyproject_toml_pep621_requirements(cls):
"""Try to get PEP 621 metadata requirements."""
Expand All @@ -569,6 +590,8 @@ def get_pyproject_toml_pep621_requirements(cls):
pep621.get("dependencies", ())))
for r in pep621.get("optional-dependencies", {}).values():
requirements.extend(parse_requirements(r))
if len(requirements) == 0:
requirements = cls.get_setuptools_dynamic_requirements()
return requirements

@classmethod
Expand Down
58 changes: 58 additions & 0 deletions test/test_pep621.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import unittest
from unittest import mock
from unittest.mock import mock_open
from unittest.mock import patch

from pkg_resources import Requirement

Check notice

Code scanning / CodeQL

Unused import Note test

Import of 'Requirement' is not used.
from pkg_resources import parse_requirements

from flake8_requirements.checker import Flake8Checker
from flake8_requirements.checker import ModuleSet
Expand Down Expand Up @@ -80,3 +84,57 @@ def test_3rd_party(self):
checker = Flake8Checker(None, None)
mods = checker.get_mods_3rd_party(False)
self.assertEqual(mods, ModuleSet({"tools": {}, "dev_tools": {}}))

def test_valid_requirements(self):
requirements_content = "package1\npackage2>=2.0"
data = {
"project": {"dynamic": ["dependencies"]},
"tool": {"setuptools": {"dynamic": {"dependencies": {"file": ["requirements.txt"]}}}}
}
with patch('flake8_requirements.checker.Flake8Checker.get_pyproject_toml', return_value=data):
with patch('builtins.open', mock_open(read_data=requirements_content)) as mocked_open:
result = Flake8Checker.get_setuptools_dynamic_requirements()
expected_results = ['package1', 'package2>=2.0']
parsed_results = [str(req) for req in result]
self.assertEqual(parsed_results, expected_results)

def test_optional_dependencies(self):
data = {
"project": {"dynamic": ["dependencies", "optional-dependencies"]},
"tool": {
"setuptools": {
"dynamic": {
"dependencies": {"file": ["requirements.txt"]},
"optional-dependencies": {"test": {"file": ["optional-requirements.txt"]}}
}
}
}
}
requirements_content = """
package1
package2>=2.0
"""
optional_requirements_content = "package3[extra] >= 3.0"
with mock.patch('flake8_requirements.checker.Flake8Checker.get_pyproject_toml', return_value=data):
with mock.patch('builtins.open', mock.mock_open()) as mocked_file:
mocked_file.side_effect = [
mock.mock_open(read_data=requirements_content).return_value,
mock.mock_open(read_data=optional_requirements_content).return_value
]
result = Flake8Checker.get_setuptools_dynamic_requirements()
expected = list(parse_requirements(requirements_content))
expected += list(parse_requirements(optional_requirements_content))

self.assertEqual(len(result), len(expected))
for i in range(len(result)):
self.assertEqual(result[i], expected[i])
#assert(sorted(result), sorted(expected))

def test_missing_requirements_file(self):
data = {
"project": {"dynamic": ["dependencies"]},
"tool": {"setuptools": {"dynamic": {"dependencies": {"file": ["nonexistent-requirements.txt"]}}}}
}
with mock.patch('flake8_requirements.checker.Flake8Checker.get_pyproject_toml', return_value=data):
result = Flake8Checker.get_setuptools_dynamic_requirements()
self.assertEqual(result, [])

0 comments on commit da4310b

Please sign in to comment.