Skip to content

Commit 7462fd1

Browse files
committed
Add in pip-extra-reqs
Check for requirements that are not used
1 parent d797afc commit 7462fd1

File tree

3 files changed

+127
-4
lines changed

3 files changed

+127
-4
lines changed

pip_check_reqs/find_extra_reqs.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import collections
2+
import logging
3+
import optparse
4+
import os
5+
import sys
6+
7+
from pip.commands.show import search_packages_info
8+
from pip.download import PipSession
9+
from pip.req import parse_requirements
10+
from pip.utils import get_installed_distributions, normalize_name
11+
12+
from pip_check_reqs import common
13+
14+
log = logging.getLogger(__name__)
15+
16+
17+
def find_extra_reqs(options):
18+
# 1. find files used by imports in the code (as best we can without
19+
# executing)
20+
used_modules = common.find_imported_modules(options)
21+
22+
# 2. find which packages provide which files
23+
installed_files = {}
24+
all_pkgs = (pkg.project_name for pkg in get_installed_distributions())
25+
for package in search_packages_info(all_pkgs):
26+
log.debug('installed package: %s (at %s)', package['name'],
27+
package['location'])
28+
for f in package['files'] or []:
29+
path = os.path.realpath(os.path.join(package['location'], f))
30+
installed_files[path] = package['name']
31+
package_path = common.is_package_file(path)
32+
if package_path:
33+
# we've seen a package file so add the bare package directory
34+
# to the installed list as well as we might want to look up
35+
# a package by its directory path later
36+
installed_files[package_path] = package['name']
37+
38+
# 3. match imported modules against those packages
39+
used = collections.defaultdict(list)
40+
for modname, info in used_modules.items():
41+
# probably standard library if it's not in the files list
42+
if info.filename in installed_files:
43+
used_name = normalize_name(installed_files[info.filename])
44+
log.debug('used module: %s (from package %s)', modname,
45+
installed_files[info.filename])
46+
used[used_name].append(info)
47+
else:
48+
log.debug(
49+
'used module: %s (from file %s, assuming stdlib or local)',
50+
modname, info.filename)
51+
52+
# 4. compare with requirements.txt
53+
explicit = set()
54+
for requirement in parse_requirements('requirements.txt',
55+
session=PipSession()):
56+
log.debug('found requirement: %s', requirement.name)
57+
explicit.add(normalize_name(requirement.name))
58+
59+
return [name for name in explicit if name not in used]
60+
61+
62+
def main():
63+
from pip_check_reqs import __version__
64+
65+
usage = 'usage: %prog [options] files or directories'
66+
parser = optparse.OptionParser(usage)
67+
parser.add_option("-f", "--ignore-file", dest="ignore_files",
68+
action="append", default=[],
69+
help="file paths globs to ignore")
70+
parser.add_option("-m", "--ignore-module", dest="ignore_mods",
71+
action="append", default=[],
72+
help="used module names (globs are ok) to ignore")
73+
parser.add_option("-v", "--verbose", dest="verbose",
74+
action="store_true", default=False, help="be more verbose")
75+
parser.add_option("-d", "--debug", dest="debug",
76+
action="store_true", default=False, help="be *really* verbose")
77+
parser.add_option("--version", dest="version",
78+
action="store_true", default=False, help="display version information")
79+
80+
(options, args) = parser.parse_args()
81+
82+
if options.version:
83+
sys.exit(__version__)
84+
85+
if not args:
86+
parser.error("no source files or directories specified")
87+
sys.exit(2)
88+
89+
options.ignore_files = common.ignorer(options.ignore_files)
90+
options.ignore_mods = common.ignorer(options.ignore_mods)
91+
92+
options.paths = args
93+
94+
logging.basicConfig(format='%(message)s')
95+
if options.debug:
96+
log.setLevel(logging.DEBUG)
97+
elif options.verbose:
98+
log.setLevel(logging.INFO)
99+
else:
100+
log.setLevel(logging.WARN)
101+
102+
log.info('using pip_check_reqs-%s from %s', __version__, __file__)
103+
104+
extras = find_extra_reqs(options)
105+
106+
if extras:
107+
log.warning('Extra requirements:')
108+
for name in extras:
109+
log.warning('%s in requirements.txt' % name)
110+
111+
if extras:
112+
sys.exit(1)
113+
114+
115+
if __name__ == '__main__': # pragma: no cover
116+
main()

setup.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
setup(
1616
name='pip_check_reqs',
1717
version=__version__,
18-
description='Find packages that should be in requirements for a project',
18+
description=
19+
'Find packages that should or should not be in requirements for a '
20+
'project',
1921
long_description=long_description,
2022
url='https://github.com/r1chardj0n3s/pip-missing-reqs',
2123
author='Richard Jonees',
@@ -34,5 +36,8 @@
3436
'console_scripts': [
3537
'pip-missing-reqs=pip_check_reqs.find_missing_reqs:main',
3638
],
39+
'console_scripts': [
40+
'pip-extra-reqs=pip_check_reqs.find_extra_reqs:main',
41+
],
3742
},
3843
)

tox.ini

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

44
[testenv]
55
deps =
@@ -21,7 +21,9 @@ norecursedirs = .git .tox *.egg build
2121
deps = flake8
2222
commands = flake8 pip_check_reqs tests
2323

24-
[testenv:pip-missing-reqs]
24+
[testenv:pip-check-reqs]
2525
# Overwrite deps so not to dirty the environment
2626
deps = -r{toxinidir}/requirements.txt
27-
commands = python -m pip_check_reqs.find_missing_reqs pip_check_reqs
27+
commands =
28+
python -m pip_check_reqs.find_missing_reqs pip_check_reqs
29+
python -m pip_check_reqs.find_extra_reqs pip_check_reqs

0 commit comments

Comments
 (0)