Skip to content

Commit b33485a

Browse files
committed
- fixed handling of import from __future__
- self-tested and added own requirements.txt - cleaned up usage to require a file or directory to scan (rather than defaulting to ".") - vendored code from pip 1.6dev which fixes bug in search_packages_info until pip 1.6 is released
1 parent 3a59678 commit b33485a

File tree

6 files changed

+115
-14
lines changed

6 files changed

+115
-14
lines changed

CHANGELOG.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Release History
2+
---------------
3+
4+
1.1.1
5+
- fixed handling of import from __future__
6+
- self-tested and added own requirements.txt
7+
- cleaned up usage to require a file or directory to scan (rather than
8+
defaulting to ".")
9+
- vendored code from pip 1.6dev which fixes bug in search_packages_info
10+
until pip 1.6 is released
11+
12+
1.1.0
13+
- implemented --ignore-module

pip_missing_reqs/find_missing_reqs.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,56 @@
22
import collections
33
import fnmatch
44
import imp
5+
import logging
56
import optparse
67
import os
78
import re
8-
import logging
9+
import sys
910

11+
import pkg_resources
1012
from pip.req import parse_requirements
1113
from pip.download import PipSession
12-
from pip.commands.show import search_packages_info
1314
from pip.util import get_installed_distributions, normalize_name
1415

15-
1616
log = logging.getLogger(__name__)
1717

1818

19+
# TODO: remove me when pip 1.6 is released (vendored from pypa/pip git)
20+
def search_packages_info(query): # pragma: no cover
21+
"""
22+
Gather details from installed distributions. Print distribution name,
23+
version, location, and installed files. Installed files requires a
24+
pip generated 'installed-files.txt' in the distributions '.egg-info'
25+
directory.
26+
"""
27+
installed = dict(
28+
[(p.project_name.lower(), p) for p in pkg_resources.working_set])
29+
query_names = [name.lower() for name in query]
30+
for dist in [installed[pkg] for pkg in query_names if pkg in installed]:
31+
package = {
32+
'name': dist.project_name,
33+
'version': dist.version,
34+
'location': dist.location,
35+
'requires': [dep.project_name for dep in dist.requires()],
36+
}
37+
file_list = None
38+
if isinstance(dist, pkg_resources.DistInfoDistribution):
39+
# RECORDs should be part of .dist-info metadatas
40+
if dist.has_metadata('RECORD'):
41+
lines = dist.get_metadata_lines('RECORD')
42+
paths = [l.split(',')[0] for l in lines]
43+
paths = [os.path.join(dist.location, p) for p in paths]
44+
file_list = [os.path.relpath(p, dist.egg_info)
45+
for p in paths]
46+
else:
47+
# Otherwise use pip's log for .egg-info's
48+
if dist.has_metadata('installed-files.txt'):
49+
file_list = dist.get_metadata_lines('installed-files.txt')
50+
# use and short-circuit to check for None
51+
package['files'] = file_list and sorted(file_list)
52+
yield package
53+
54+
1955
class FoundModule:
2056
def __init__(self, modname, filename, locations=None):
2157
self.modname = modname
@@ -41,6 +77,9 @@ def visit_Import(self, node):
4177
self.__addModule(alias.name, node.lineno)
4278

4379
def visit_ImportFrom(self, node):
80+
if node.module == '__future__':
81+
# not an actual module
82+
return
4483
for alias in node.names:
4584
self.__addModule(node.module + '.' + alias.name, node.lineno)
4685

@@ -180,7 +219,8 @@ def f(candidate, ignore_cfg=ignore_cfg):
180219

181220

182221
def main():
183-
parser = optparse.OptionParser()
222+
usage = 'usage: %prog [options] files or directories'
223+
parser = optparse.OptionParser(usage)
184224
parser.add_option("-f", "--ignore-file", dest="ignore_files",
185225
action="append", default=[],
186226
help="file paths globs to ignore")
@@ -191,6 +231,11 @@ def main():
191231
action="store_true", default=False, help="be more verbose")
192232

193233
(options, args) = parser.parse_args()
234+
235+
if not args:
236+
parser.error("no source files or directories specified")
237+
sys.exit(-1)
238+
194239
options.ignore_files = ignorer(options.ignore_files)
195240
options.ignore_mods = ignorer(options.ignore_mods)
196241

pip_missing_reqs/test_find_missing_reqs.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,10 @@ def test_pyfiles_package(monkeypatch):
8787

8888

8989
@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)]),
90+
(False, False, ['ast', 'os', 'hashlib'], [('spam.py', 2), ('ham.py', 2)]),
91+
(False, True, ['ast', 'os'], [('spam.py', 2), ('ham.py', 2)]),
92+
(True, False, ['ast'], [('spam.py', 2)]),
93+
(True, True, ['ast'], [('spam.py', 2)]),
9494
])
9595
def test_find_imported_modules(monkeypatch, caplog, ignore_ham, ignore_hashlib,
9696
expect, locs):
@@ -104,7 +104,7 @@ def test_find_imported_modules(monkeypatch, caplog, ignore_ham, ignore_hashlib,
104104
class FakeFile():
105105
contents = [
106106
'from os import path\nimport ast, hashlib',
107-
'import ast, sys',
107+
'from __future__ import braces\nimport ast, sys',
108108
]
109109

110110
def __init__(self, filename):
@@ -190,11 +190,14 @@ class options:
190190
options = options()
191191

192192
class FakeOptParse:
193+
def __init__(self, usage):
194+
pass
195+
193196
def add_option(*args, **kw):
194197
pass
195198

196199
def parse_args(self):
197-
return [options, 'ham.py']
200+
return (options, ['ham.py'])
198201

199202
monkeypatch.setattr(optparse, 'OptionParser', FakeOptParse)
200203

@@ -211,6 +214,30 @@ def parse_args(self):
211214
'location.py:1 dist=missing module=missing'
212215

213216

217+
def test_main_no_spec(monkeypatch, caplog):
218+
class FakeOptParse:
219+
def __init__(self, usage):
220+
pass
221+
222+
def add_option(*args, **kw):
223+
pass
224+
225+
def parse_args(self):
226+
return (None, [])
227+
228+
error = pretend.call_recorder(lambda *a: None)
229+
230+
monkeypatch.setattr(optparse, 'OptionParser', FakeOptParse)
231+
monkeypatch.setattr(find_missing_reqs, 'ignorer',
232+
pretend.call_recorder(lambda a: None))
233+
234+
with pytest.raises(SystemExit):
235+
find_missing_reqs.main()
236+
237+
assert FakeOptParse.error.calls
238+
assert not find_missing_reqs.ignorer.calls
239+
240+
214241
@pytest.mark.parametrize(["ignore_cfg", "candidate", "result"], [
215242
([], 'spam', False),
216243
([], 'ham', False),
@@ -220,8 +247,10 @@ def parse_args(self):
220247
(['spam*'], 'spam', True),
221248
(['spam*'], 'spam.ham', True),
222249
(['spam*'], 'eggs', False),
250+
(['spam'], '/spam', True),
223251
])
224252
def test_ignorer(monkeypatch, ignore_cfg, candidate, result):
253+
monkeypatch.setattr(os.path, 'relpath', lambda s: s.lstrip('/'))
225254
ignorer = find_missing_reqs.ignorer(ignore_cfg)
226255
assert ignorer(candidate) == result
227256

@@ -239,11 +268,14 @@ class options:
239268
options = options()
240269

241270
class FakeOptParse:
271+
def __init__(self, usage):
272+
pass
273+
242274
def add_option(*args, **kw):
243275
pass
244276

245277
def parse_args(self):
246-
return [options, 'ham.py']
278+
return (options, ['ham.py'])
247279

248280
monkeypatch.setattr(optparse, 'OptionParser', FakeOptParse)
249281

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pip>1.5

setup.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
88
long_description = f.read()
99

10+
with open(path.join(here, 'CHANGELOG.rst'), encoding='utf-8') as f:
11+
long_description += f.read()
12+
1013
setup(
1114
name='pip_missing_reqs',
12-
version='1.1.0',
15+
version='1.1.1',
1316
description='Find packages that should be in requirements for a project',
1417
long_description=long_description,
1518
url='https://github.com/r1chardj0n3s/pip-missing-reqs',

tox.ini

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py27,py34,pep8
2+
envlist = py27,py34,pep8,pip-missing-reqs
33

44
[testenv]
55
deps =
@@ -8,7 +8,8 @@ deps =
88
pytest
99
pytest-capturelog
1010
commands =
11-
coverage run --source find_missing_reqs.py -m pytest --strict []
11+
coverage run --source pip_missing_reqs/find_missing_reqs.py \
12+
-m pytest --strict []
1213
coverage report -m --fail-under 100
1314

1415
[flake8]
@@ -21,3 +22,9 @@ norecursedirs = .git .tox *.egg build
2122
[testenv:pep8]
2223
deps = flake8
2324
commands = flake8 pip_missing_reqs
25+
26+
[testenv:pip-missing-reqs]
27+
deps = pip_missing_reqs
28+
commands = pip-missing-reqs --ignore-file=pip_missing_reqs/test* \
29+
pip_missing_reqs
30+

0 commit comments

Comments
 (0)