From 276e5435a12c0eea698a4ee63aa9dcfc03cce19d Mon Sep 17 00:00:00 2001 From: Mihail Andreev Date: Fri, 26 Apr 2024 11:47:10 +0300 Subject: [PATCH 1/2] Add support for dynamic dependencies 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. --- src/flake8_requirements/checker.py | 29 ++++++++++ test/test_pep621.py | 87 ++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/src/flake8_requirements/checker.py b/src/flake8_requirements/checker.py index c3c595f..3a67d99 100644 --- a/src/flake8_requirements/checker.py +++ b/src/flake8_requirements/checker.py @@ -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, []) From 1ee3153a82a0bb7150f235c6687978551c4c1c9d Mon Sep 17 00:00:00 2001 From: Arkadiusz Bokowy Date: Wed, 8 May 2024 13:31:26 +0200 Subject: [PATCH 2/2] Bump version --- src/flake8_requirements/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flake8_requirements/checker.py b/src/flake8_requirements/checker.py index 3a67d99..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')