Skip to content

Commit

Permalink
Load requirements from setup configuration file
Browse files Browse the repository at this point in the history
Closes #39
  • Loading branch information
arkq committed Jun 20, 2021
1 parent 0e3b5fc commit c29f14c
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 27 deletions.
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License

Copyright (c) 2017-2020 Arkadiusz Bokowy <[email protected]>
Copyright (c) 2017-2021 Arkadiusz Bokowy <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[bdist_wheel]
universal = 1

[isort]
force_single_line = true
90 changes: 65 additions & 25 deletions src/flake8_requirements/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import site
import sys
from collections import namedtuple
from configparser import ConfigParser
from functools import wraps
from logging import getLogger

Expand All @@ -17,7 +18,7 @@
from .modules import STDLIB_PY3

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

LOG = getLogger('flake8.plugin.requirements')
Expand Down Expand Up @@ -530,6 +531,16 @@ def get_pyproject_toml_poetry(cls):
cfg_pep518 = cls.get_pyproject_toml()
return cfg_pep518.get('tool', {}).get('poetry', {})

def get_pyproject_toml_poetry_requirements(self):
"""Try to get poetry configuration requirements."""
poetry = self.get_pyproject_toml_poetry()
requirements = []
requirements.extend(parse_requirements(
poetry.get('dependencies', ())))
requirements.extend(parse_requirements(
poetry.get('dev-dependencies', ())))
return requirements

@classmethod
@memoize
def get_requirements_txt(cls):
Expand All @@ -544,6 +555,38 @@ def get_requirements_txt(cls):
LOG.error("Couldn't load requirements: %s", e)
return ()

@classmethod
@memoize
def get_setup_cfg(cls):
"""Try to load standard configuration file."""
config = ConfigParser()
config.read_dict({
'metadata': {'name': ""},
'options': {
'install_requires': "",
'setup_requires': "",
'tests_require': ""},
'options.extras_require': {},
})
if not config.read(os.path.join(cls.root_dir, "setup.cfg")):
LOG.debug("Couldn't load setup configuration: setup.cfg")
return config

def get_setup_cfg_requirements(self):
"""Try to load standard configuration file requirements."""
config = self.get_setup_cfg()
requirements = []
requirements.extend(parse_requirements(
config.get('options', 'install_requires')))
requirements.extend(parse_requirements(
config.get('options', 'tests_require')))
for _, r in config.items('options.extras_require'):
requirements.extend(parse_requirements(r))
setup_requires = config.get('options', 'setup_requires')
if setup_requires and self.processing_setup_py:
requirements.extend(parse_requirements(setup_requires))
return requirements

@classmethod
@memoize
def get_setup_py(cls):
Expand All @@ -555,13 +598,24 @@ def get_setup_py(cls):
LOG.debug("Couldn't load project setup: %s", e)
return SetupVisitor(ast.parse(""), cls.root_dir)

def get_setup_py_requirements(self):
"""Try to load standard setup file requirements."""
setup = self.get_setup_py()
if not setup.redirected:
return []
return setup.get_requirements(
setup=self.processing_setup_py,
tests=True,
)

@classmethod
@memoize
def get_mods_1st_party(cls):
mods_1st_party = ModuleSet()
# Get 1st party modules (used for absolute imports).
modules = [project2module(
cls.get_setup_py().keywords.get('name') or
cls.get_setup_cfg().get('metadata', 'name') or
cls.get_pyproject_toml_poetry().get('name') or
"")]
if modules[0] in cls.known_modules:
Expand All @@ -572,33 +626,19 @@ def get_mods_1st_party(cls):

def get_mods_3rd_party_requirements(self):
"""Get list of 3rd party requirements."""

# Use user provided requirements text file.
if self.requirements_file:
return self.get_requirements_txt()

# Use requirements from setup if available.
cfg_setup = self.get_setup_py()
if cfg_setup.redirected:
return cfg_setup.get_requirements(
setup=self.processing_setup_py,
tests=True,
)

# Check project configuration for requirements.
cfg_poetry = self.get_pyproject_toml_poetry()
if cfg_poetry:
requirements = []
requirements.extend(parse_requirements(
cfg_poetry.get('dependencies', ()),
))
requirements.extend(parse_requirements(
cfg_poetry.get('dev-dependencies', ()),
))
return requirements

# Fall-back to requirements.txt in the root directory.
return self.get_requirements_txt()
return (
# Use requirements from setup if available.
self.get_setup_py_requirements() or
# Check setup configuration file for requirements.
self.get_setup_cfg_requirements() or
# Check project configuration for requirements.
self.get_pyproject_toml_poetry_requirements() or
# Fall-back to requirements.txt in our root directory.
self.get_requirements_txt()
)

@memoize
def get_mods_3rd_party(self):
Expand Down
2 changes: 2 additions & 0 deletions test/test_poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ 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=content).return_value,
)

Expand All @@ -43,6 +44,7 @@ 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=content).return_value,
)

Expand Down
23 changes: 23 additions & 0 deletions test/test_setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[metadata]
name = my_package
description = My package description
license = BSD 3-Clause License
classifiers =
License :: OSI Approved :: BSD License
Programming Language :: Python :: 3

[options]
packages = find:
install_requires =
requests
importlib; python_version == "2.6"
tests_require =
pytest

[options.package_data]
* = *.txt, *.rst
hello = *.msg

[options.extras_require]
pdf = ReportLab>=1.2
rest = docutils>=0.3
28 changes: 27 additions & 1 deletion test/test_setup.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import ast
import os
import unittest

from pkg_resources import parse_requirements

from flake8_requirements.checker import Flake8Checker
from flake8_requirements.checker import SetupVisitor

try:
from unittest import mock
builtins_open = 'builtins.open'
except ImportError:
import mock
builtins_open = '__builtin__.open'

class Flake8CheckerTestCase(unittest.TestCase):

class SetupTestCase(unittest.TestCase):

def test_detect_setup(self):
code = "setup({})".format(",".join((
Expand Down Expand Up @@ -65,3 +74,20 @@ def test_get_requirements(self):
"extra < 10",
]), key=lambda x: x.project_name),
)

def test_get_setup_cfg_requirements(self):
curdir = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(curdir, "test_setup.cfg")) as f:
content = f.read()
with mock.patch(builtins_open, mock.mock_open(read_data=content)):
checker = Flake8Checker(None, None)
self.assertEqual(
checker.get_setup_cfg_requirements(),
list(parse_requirements([
"requests",
"importlib; python_version == \"2.6\"",
"pytest",
"ReportLab>=1.2",
"docutils>=0.3",
])),
)

0 comments on commit c29f14c

Please sign in to comment.