diff --git a/src/flake8_requirements/checker.py b/src/flake8_requirements/checker.py index c3c595f..ad8a733 100644 --- a/src/flake8_requirements/checker.py +++ b/src/flake8_requirements/checker.py @@ -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') @@ -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.""" @@ -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 diff --git a/test/test_pep621.py b/test/test_pep621.py index 9148c8a..22e9bc3 100644 --- a/test/test_pep621.py +++ b/test/test_pep621.py @@ -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 @@ -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, [])