Skip to content

Commit

Permalink
Use setup requirements when checking setup.py
Browse files Browse the repository at this point in the history
  • Loading branch information
arkq committed Nov 27, 2017
1 parent 83b8774 commit 20d47b9
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 26 deletions.
48 changes: 42 additions & 6 deletions src/flake8_requirements/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import sys
from collections import namedtuple
from itertools import chain
from logging import getLogger
from os import path

from pkg_resources import parse_requirements

Expand All @@ -14,6 +16,8 @@
__version__ = "1.0.0"
__license__ = "MIT"

LOG = getLogger('flake8.plugin.requires')

ERRORS = {
'I900': "I900 '{pkg}' not listed as a requirement",
'I901': "I901 '{pkg}' required but not used",
Expand Down Expand Up @@ -147,7 +151,7 @@ def setup(**kw):
{'__file__': "setup.py", '__f8r_setup': setup},
)

def get_requirements(self, install=True, extras=True):
def get_requirements(self, install=True, extras=True, setup=False):
"""Get package requirements."""
requires = []
if install:
Expand All @@ -157,6 +161,10 @@ def get_requirements(self, install=True, extras=True):
if extras:
for r in self.keywords.get('extras_require', {}).values():
requires.extend(parse_requirements(r))
if setup:
requires.extend(parse_requirements(
self.keywords.get('setup_requires', ()),
))
return requires

def visit_Call(self, node):
Expand Down Expand Up @@ -202,8 +210,9 @@ class Flake8Checker(object):

def __init__(self, tree, filename, lines=None):
"""Initialize requirements checker."""
self.setup = self.get_setup()
self.tree = tree
self.filename = filename
self.setup = self.get_setup()

@classmethod
def add_options(cls, manager):
Expand Down Expand Up @@ -231,10 +240,25 @@ def parse_options(cls, options):
]
}

def get_setup(self):
@classmethod
def get_setup(cls):
"""Get package setup."""
with open("setup.py") as f:
return SetupVisitor(ast.parse(f.read()))
if not getattr(cls, '_setup', None):
try:
with open("setup.py") as f:
cls._setup = SetupVisitor(ast.parse(f.read()))
except IOError as e:
LOG.warning("Couldn't open setup file: %s", e)
cls._setup = SetupVisitor(ast.parse(""))
return cls._setup

@property
def processing_setup_py(self):
"""Determine whether we are processing setup.py file."""
try:
return path.samefile(self.filename, "setup.py")
except OSError:
return False

def run(self):
"""Run checker."""
Expand All @@ -258,15 +282,27 @@ def modcmp(lib=(), test=()):
modules = self.known_modules[modules[0]]
mods_1st_party.update(split(x) for x in modules)

requirements = self.setup.get_requirements(
setup=self.processing_setup_py,
)

# Get 3rd party module names based on requirements.
for requirement in self.setup.get_requirements():
for requirement in requirements:
modules = [project2module(requirement.project_name)]
if modules[0] in KNOWN_3RD_PARTIES:
modules = KNOWN_3RD_PARTIES[modules[0]]
if modules[0] in self.known_modules:
modules = self.known_modules[modules[0]]
mods_3rd_party.update(split(x) for x in modules)

# When processing setup.py file, forcefully add setuptools to the
# project requirements. Setuptools might be required to build the
# project, even though it is not listed as a requirement - this
# package is required to run setup.py, so listing it as a setup
# requirement would be pointless.
if self.processing_setup_py:
mods_3rd_party.add(split("setuptools"))

for node in ImportVisitor(self.tree).imports:
_mod = split(node.mod)
_alt = split(node.alt)
Expand Down
54 changes: 34 additions & 20 deletions test/test_checker.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,38 @@
import ast
import unittest

from pkg_resources import parse_requirements

from flake8_requirements import checker


class SetupVisitorMock:
keywords = {
'name': "flake8-requires",
}
class SetupVisitorMock(checker.SetupVisitor):

def get_requirements(self):
return parse_requirements((
"foo",
"bar",
"hyp-hen",
"python-boom",
"setuptools",
"space.module",
))
def __init__(self):
self.keywords = {
'name': "flake8-requires",
'install_requires': [
"foo",
"bar",
"hyp-hen",
"python-boom",
"pillow",
"space.module",
],
}


class Flake8Checker(checker.Flake8Checker):
def get_setup(self):

@classmethod
def get_setup(cls):
return SetupVisitorMock()

@property
def processing_setup_py(self):
return self.filename == "setup.py"


def check(code):
return list(Flake8Checker(ast.parse(code), "<unknown>").run())
def check(code, filename="<unknown>"):
return list(Flake8Checker(ast.parse(code), filename).run())


class Flake8CheckerTestCase(unittest.TestCase):
Expand Down Expand Up @@ -71,8 +75,8 @@ def test_3rd_party_hyphen(self):
errors = check("from hyp_hen import Hyphen")
self.assertEqual(len(errors), 0)

def test_3rd_party_multi_module(self):
errors = check("import pkg_resources")
def test_3rd_party_known_module(self):
errors = check("import PIL")
self.assertEqual(len(errors), 0)

def test_non_top_level_import(self):
Expand All @@ -96,3 +100,13 @@ def test_relative(self):
self.assertEqual(len(errors), 0)
errors = check("from ..local import local")
self.assertEqual(len(errors), 0)

def test_setup_py(self):
errors = check("from setuptools import setup", "setup.py")
self.assertEqual(len(errors), 0)
errors = check("from setuptools import setup", "xxx.py")
self.assertEqual(len(errors), 1)
self.assertEqual(
errors[0][2],
"I900 'setuptools' not listed as a requirement",
)

0 comments on commit 20d47b9

Please sign in to comment.