Skip to content

Commit 3a59678

Browse files
committed
implement --ignore-module; improve docs
1 parent ba80121 commit 3a59678

File tree

5 files changed

+79
-56
lines changed

5 files changed

+79
-56
lines changed

README.rst

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
pip-missing-reqs
22
================
33

4-
Find packages that should be in requirements for a project.
4+
It happens: you start using a module in your project and it works and you
5+
don't realise that it's only being included in your `virtualenv`_ because
6+
it's a dependency of a package you're using. This tool finds those modules so
7+
you can include them in the `requirements.txt`_ for the project.
8+
9+
.. _`virtualenv`: https://virtualenv.pypa.io/en/latest/
10+
.. _`requirements.txt`: https://pip.pypa.io/en/latest/user_guide.html#requirements-files
511

612
Assuming your project follows a layout like the suggested `sample project`_::
713

@@ -17,7 +23,7 @@ Assuming your project follows a layout like the suggested `sample project`_::
1723
Basic usage, running in your project directory::
1824

1925
<activate virtualenv for your project>
20-
pip-missing-reqs --ignore-files=sample/tests sample
26+
pip-missing-reqs --ignore-files=sample/tests/* sample
2127

2228
This will find all imports in the code in "sample" and check that the
2329
packages those modules belong to are in the requirements.txt file.
@@ -30,7 +36,7 @@ To make your life easier, copy something like this into your tox.ini::
3036

3137
[pip-missing-reqs]
3238
deps=-rrequirements.txt
33-
commands=pip-missing-reqs --ignore-files=sample/tests sample
39+
commands=pip-missing-reqs --ignore-files=sample/tests/* sample
3440

3541

3642
Excluding test files (or others) from this check
@@ -43,3 +49,16 @@ don't want this tool to generate false hits for those.
4349

4450
You may exclude those test files from your check using the --ignore-files
4551
option.
52+
53+
54+
Excluding modules from the check
55+
--------------------------------
56+
57+
If your project has modules which are conditionally imported, or requirements
58+
which are conditionally included, you may exclude certain modules from the
59+
check by name (or glob pattern) using --ignore-mods::
60+
61+
# ignore the module spam
62+
pip-missing-reqs --ignore-mods=spam sample
63+
# ignore the whole package spam as well
64+
pip-missing-reqs --ignore-mods=spam --ignore-mods=spam.* sample

pip_missing_reqs/find_missing_reqs.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ def __repr__(self):
2727

2828

2929
class ImportVisitor(ast.NodeVisitor):
30-
def __init__(self):
30+
def __init__(self, options):
3131
super(ImportVisitor, self).__init__()
32+
self.__options = options
3233
self.__modules = {}
3334
self.__location = None
3435

@@ -44,6 +45,8 @@ def visit_ImportFrom(self, node):
4445
self.__addModule(node.module + '.' + alias.name, node.lineno)
4546

4647
def __addModule(self, modname, lineno):
48+
if self.__options.ignore_mods(modname):
49+
return
4750
path = None
4851
progress = []
4952
modpath = last_modpath = None
@@ -102,7 +105,7 @@ def pyfiles(root):
102105

103106

104107
def find_imported_modules(options):
105-
vis = ImportVisitor()
108+
vis = ImportVisitor(options)
106109
for path in options.paths:
107110
for filename in pyfiles(path):
108111
if options.ignore_files(filename):
@@ -162,30 +165,34 @@ def find_missing_reqs(options):
162165
yield name, used[name]
163166

164167

168+
def ignorer(ignore_cfg):
169+
if not ignore_cfg:
170+
return lambda candidate: False
171+
172+
def f(candidate, ignore_cfg=ignore_cfg):
173+
for ignore in ignore_cfg:
174+
if fnmatch.fnmatch(candidate, ignore):
175+
return True
176+
elif fnmatch.fnmatch(os.path.relpath(candidate), ignore):
177+
return True
178+
return False
179+
return f
180+
181+
165182
def main():
166183
parser = optparse.OptionParser()
167184
parser.add_option("-f", "--ignore-file", dest="ignore_files",
168185
action="append", default=[],
169-
help="file paths (globs or fragments) to ignore")
186+
help="file paths globs to ignore")
170187
parser.add_option("-m", "--ignore-module", dest="ignore_mods",
171188
action="append", default=[],
172-
help="used modules (by name) to ignore")
189+
help="used module names (globs are ok) to ignore")
173190
parser.add_option("-v", "--verbose", dest="verbose",
174191
action="store_true", default=False, help="be more verbose")
175192

176193
(options, args) = parser.parse_args()
177-
if options.ignore_files:
178-
def ignore_files(filename, ignore_files=options.ignore_files):
179-
for ignore in ignore_files:
180-
if '*' in ignore:
181-
if fnmatch.fnmatch(filename, ignore):
182-
return True
183-
elif ignore in filename:
184-
return True
185-
return False
186-
options.ignore_files = ignore_files
187-
else:
188-
options.ignore_files = lambda x: False
194+
options.ignore_files = ignorer(options.ignore_files)
195+
options.ignore_mods = ignorer(options.ignore_mods)
189196

190197
options.paths = args or ['.']
191198

pip_missing_reqs/test_find_missing_reqs.py

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ def test_FoundModule():
4545
('import spam', []), # don't break because bad programmer
4646
])
4747
def test_ImportVisitor(stmt, result):
48-
vis = find_missing_reqs.ImportVisitor()
48+
class options:
49+
def ignore_mods(self, modname):
50+
return False
51+
vis = find_missing_reqs.ImportVisitor(options())
4952
vis.set_location('spam.py')
5053
vis.visit(ast.parse(stmt))
5154
result = vis.finalise()
@@ -83,22 +86,24 @@ def test_pyfiles_package(monkeypatch):
8386
['spam/__init__.py', 'spam/ham.py', 'spam/dub/bass.py']
8487

8588

86-
@pytest.mark.parametrize(["ignore_ham", "result_keys", "locs"], [
87-
(False, ['ast', 'os'], [('spam.py', 1), ('ham.py', 2)]),
88-
(True, ['ast'], [('spam.py', 1)]),
89+
@pytest.mark.parametrize(["ignore_ham", "ignore_hashlib", "expect", "locs"], [
90+
(False, False, ['ast', 'os', 'hashlib'], [('spam.py', 1), ('ham.py', 2)]),
91+
(False, True, ['ast', 'os'], [('spam.py', 1), ('ham.py', 2)]),
92+
(True, False, ['ast'], [('spam.py', 1)]),
93+
(True, True, ['ast'], [('spam.py', 1)]),
8994
])
90-
def test_find_imported_modules(monkeypatch, caplog, ignore_ham, result_keys,
91-
locs):
95+
def test_find_imported_modules(monkeypatch, caplog, ignore_ham, ignore_hashlib,
96+
expect, locs):
9297
monkeypatch.setattr(find_missing_reqs, 'pyfiles',
9398
pretend.call_recorder(lambda x: ['spam.py', 'ham.py']))
9499

95100
if sys.version_info[0] == 2:
96101
# py2 will find sys module but py3k won't
97-
result_keys.append('sys')
102+
expect.append('sys')
98103

99104
class FakeFile():
100105
contents = [
101-
'from os import path\nimport ast',
106+
'from os import path\nimport ast, hashlib',
102107
'import ast, sys',
103108
]
104109

@@ -127,8 +132,14 @@ def ignore_files(path):
127132
return True
128133
return False
129134

135+
@staticmethod
136+
def ignore_mods(module):
137+
if module == 'hashlib' and ignore_hashlib:
138+
return True
139+
return False
140+
130141
result = find_missing_reqs.find_imported_modules(options)
131-
assert set(result) == set(result_keys)
142+
assert set(result) == set(expect)
132143
assert result['ast'].locations == locs
133144

134145
if ignore_ham:
@@ -200,33 +211,19 @@ def parse_args(self):
200211
'location.py:1 dist=missing module=missing'
201212

202213

203-
@pytest.mark.parametrize(["ignore_cfg", "file_candidates"], [
204-
([], [('spam', False), ('ham', False)]),
205-
(['spam'], [('spam', True), ('ham', False), ('eggs', False)]),
206-
(['*am'], [('spam', True), ('ham', True), ('eggs', False)]),
214+
@pytest.mark.parametrize(["ignore_cfg", "candidate", "result"], [
215+
([], 'spam', False),
216+
([], 'ham', False),
217+
(['spam'], 'spam', True),
218+
(['spam'], 'spam.ham', False),
219+
(['spam'], 'eggs', False),
220+
(['spam*'], 'spam', True),
221+
(['spam*'], 'spam.ham', True),
222+
(['spam*'], 'eggs', False),
207223
])
208-
def test_ignore_files(monkeypatch, ignore_cfg, file_candidates):
209-
class options:
210-
paths = ['dummy']
211-
verbose = True
212-
ignore_files = ignore_cfg
213-
ignore_mods = []
214-
options = options()
215-
216-
class FakeOptParse:
217-
def add_option(*args, **kw):
218-
pass
219-
220-
def parse_args(self):
221-
return [options, 'ham.py']
222-
223-
monkeypatch.setattr(optparse, 'OptionParser', FakeOptParse)
224-
225-
monkeypatch.setattr(find_missing_reqs, 'find_missing_reqs', lambda x: [])
226-
find_missing_reqs.main()
227-
228-
for fn, matched in file_candidates:
229-
assert options.ignore_files(fn) == matched
224+
def test_ignorer(monkeypatch, ignore_cfg, candidate, result):
225+
ignorer = find_missing_reqs.ignorer(ignore_cfg)
226+
assert ignorer(candidate) == result
230227

231228

232229
@pytest.mark.parametrize(["verbose_cfg", "events", "result"], [

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
setup(
1111
name='pip_missing_reqs',
12-
version='1.0.0',
12+
version='1.1.0',
1313
description='Find packages that should be in requirements for a project',
1414
long_description=long_description,
1515
url='https://github.com/r1chardj0n3s/pip-missing-reqs',

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ ignore = E128,E126
1616
exclude = .tox,*.egg
1717

1818
[pytest]
19-
norecursedirs = .git .tox *.egg
19+
norecursedirs = .git .tox *.egg build
2020

2121
[testenv:pep8]
2222
deps = flake8

0 commit comments

Comments
 (0)