From c43dc967480e5cf2bb73ec0aa2a0d0b074f1b639 Mon Sep 17 00:00:00 2001 From: Tom Halstead Date: Thu, 30 Jun 2022 00:57:30 -0700 Subject: [PATCH] Add PEP 621 as a source for modules --- README.rst | 1 + src/flake8_requirements/checker.py | 21 +++++++++- test/test_pep621.py | 63 ++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 test/test_pep621.py diff --git a/README.rst b/README.rst index 6711eb2..1a29f9e 100644 --- a/README.rst +++ b/README.rst @@ -16,6 +16,7 @@ In order to collect project's dependencies, this checker evaluates Python code f ``setup.py`` file stored in the project's root directory. Code evaluation is done with the `eval() `_ function. As a fall-back method, this checker also tries to load dependencies, in order, from the ``setup.cfg``, the ``pyproject.toml`` +file from the `PEP 621 `_ project section, the ``pyproject.toml`` file from the `poetry `_ tool section, or from the ``requirements.txt`` text file in the project's root directory. diff --git a/src/flake8_requirements/checker.py b/src/flake8_requirements/checker.py index 4d60c11..88c854b 100644 --- a/src/flake8_requirements/checker.py +++ b/src/flake8_requirements/checker.py @@ -18,7 +18,7 @@ from .modules import STDLIB_PY3 # NOTE: Changing this number will alter package version as well. -__version__ = "1.5.5" +__version__ = "1.6.0" __license__ = "MIT" LOG = getLogger('flake8.plugin.requirements') @@ -536,6 +536,22 @@ def get_pyproject_toml(cls): LOG.debug("Couldn't load project setup: %s", e) return {} + @classmethod + def get_pyproject_toml_pep621(cls): + """Try to get PEP 621 metadata.""" + cfg_pep518 = cls.get_pyproject_toml() + return cfg_pep518.get('project', {}) + + def get_pyproject_toml_pep621_requirements(self): + """Try to get PEP 621 metadata requirements.""" + pep621 = self.get_pyproject_toml_pep621() + requirements = [] + requirements.extend(parse_requirements( + pep621.get("dependencies", ()))) + for r in pep621.get("optional-dependencies", {}).values(): + requirements.extend(parse_requirements(r)) + return requirements + @classmethod def get_pyproject_toml_poetry(cls): """Try to get poetry configuration.""" @@ -627,6 +643,7 @@ def get_mods_1st_party(cls): modules = [project2module( cls.get_setup_py().keywords.get('name') or cls.get_setup_cfg().get('metadata', 'name') or + cls.get_pyproject_toml_pep621().get('name') or cls.get_pyproject_toml_poetry().get('name') or "")] if modules[0] in cls.known_modules: @@ -645,6 +662,8 @@ def get_mods_3rd_party_requirements(self): self.get_setup_py_requirements() or # Check setup configuration file for requirements. self.get_setup_cfg_requirements() or + # Check PEP 621 metadata for requirements. + self.get_pyproject_toml_pep621_requirements() or # Check project configuration for requirements. self.get_pyproject_toml_poetry_requirements() or # Fall-back to requirements.txt in our root directory. diff --git a/test/test_pep621.py b/test/test_pep621.py new file mode 100644 index 0000000..570ace0 --- /dev/null +++ b/test/test_pep621.py @@ -0,0 +1,63 @@ +import unittest + +from flake8_requirements.checker import Flake8Checker +from flake8_requirements.checker import ModuleSet +from flake8_requirements.checker import memoize + +try: + from unittest import mock + builtins_open = 'builtins.open' +except ImportError: + import mock + builtins_open = '__builtin__.open' + + +class Pep621TestCase(unittest.TestCase): + + content = """ + [project] + name="test" + dependencies=["tools==1.0"] + + [project.optional-dependencies] + dev = ["dev-tools==1.0"] + """ + + def setUp(self): + memoize.mem = {} + + def test_get_pyproject_toml_pep621(self): + with mock.patch(builtins_open, mock.mock_open(read_data=self.content)): + pep621 = Flake8Checker.get_pyproject_toml_pep621() + expected = { + "name": "test", + "dependencies": ["tools==1.0"], + "optional-dependencies": { + "dev": ["dev-tools==1.0"] + }, + } + self.assertDictEqual(pep621, expected) + + def test_1st_party(self): + with mock.patch(builtins_open, mock.mock_open()) as m: + m.side_effect = ( + IOError("No such file or directory: 'setup.py'"), + IOError("No such file or directory: 'setup.cfg'"), + mock.mock_open(read_data=self.content).return_value, + ) + + checker = Flake8Checker(None, None) + mods = checker.get_mods_1st_party() + self.assertEqual(mods, ModuleSet({"test": {}})) + + def test_3rd_party(self): + with mock.patch(builtins_open, mock.mock_open()) as m: + m.side_effect = ( + IOError("No such file or directory: 'setup.py'"), + IOError("No such file or directory: 'setup.cfg'"), + mock.mock_open(read_data=self.content).return_value, + ) + + checker = Flake8Checker(None, None) + mods = checker.get_mods_3rd_party() + self.assertEqual(mods, ModuleSet({"tools": {}, "dev_tools": {}}))