Skip to content

Commit

Permalink
Add support for dynamic dependencies (#91)
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
miAndreev authored May 8, 2024
1 parent f7f0c97 commit 752581c
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 1 deletion.
31 changes: 30 additions & 1 deletion src/flake8_requirements/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from .modules import STDLIB_PY3

# NOTE: Changing this number will alter package version as well.
__version__ = "2.1.1"
__version__ = "2.2.0"
__license__ = "MIT"

LOG = getLogger('flake8.plugin.requirements')
Expand Down Expand Up @@ -560,6 +560,33 @@ 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 config."""
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 +596,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
87 changes: 87 additions & 0 deletions test/test_pep621.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import unittest
from unittest import mock
from unittest.mock import mock_open
from unittest.mock import patch

from pkg_resources import parse_requirements

from flake8_requirements.checker import Flake8Checker
from flake8_requirements.checker import ModuleSet
Expand Down Expand Up @@ -80,3 +83,87 @@ 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_dynamic_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)
):
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_dynamic_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])

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 752581c

Please sign in to comment.