Skip to content
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

Add support for dynamic dependencies #91

Merged
merged 2 commits into from
May 8, 2024
Merged
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
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, [])
Loading