---
Namcap/rules/__init__.py | 1 +
Namcap/rules/pydepends.py | 122 +
Namcap/tests/package/test_pydepends.py | 68 ++
3 files changed, 191 insertions(+)
create mode 100644 Namcap/rules/pydepends.py
create mode 100644 Namcap/tests/package/test_pydepends.py
diff --git a/Namcap/rules/__init__.py b/Namcap/rules/__init__.py
index 525dbc6..01d1b96 100644
--- a/Namcap/rules/__init__.py
+++ b/Namcap/rules/__init__.py
@@ -43,6 +43,7 @@ from . import (
perllocal,
permissions,
py_mtime,
+ pydepends,
rpath,
scrollkeeper,
shebangdepends,
diff --git a/Namcap/rules/pydepends.py b/Namcap/rules/pydepends.py
new file mode 100644
index 000..efc6735
--- /dev/null
+++ b/Namcap/rules/pydepends.py
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+#
+# namcap rules - pydepends
+# Copyright (C) 2020 Felix Yan
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+
+from collections import defaultdict
+import ast
+import sys
+import sysconfig
+import Namcap.package
+from Namcap.ruleclass import *
+
+
+def finddepends(liblist):
+ """
+ Find packages owning a list of libraries
+
+ Returns:
+ dependlist -- a dictionary { package => set(libraries) }
+ orphans -- the list of libraries without owners
+ """
+ dependlist = defaultdict(set)
+
+ pymatches = {}
+
+ knownlibs = set(liblist)
+ foundlibs = set()
+
+ workarounds = {
+ "python": sys.builtin_module_names
+ }
+
+ for pkg in Namcap.package.get_installed_packages():
+ for j, fsize, fmode in pkg.files:
+ if not j.startswith("usr/lib/python3"):
+ continue
+
+ for k in knownlibs:
+ if j.endswith("site-packages/" + k + "/") or
j.endswith("site-packages/" + k + ".py") or \
+ j.endswith("site-packages/" + k
+ ".so") or \
+ j.endswith("site-packages/" + k
+ sysconfig.get_config_var('EXT_SUFFIX')) or \
+ j.endswith("lib-dynload/" + k +
sysconfig.get_config_var('EXT_SUFFIX')) or \
+ j.count("/") == 3 and
j.endswith("/" + k + ".py") or \
+ j.count("/") == 4 and
j.endswith("/" + k + "/") or \
+ pkg.name in workarounds and k
in workarounds[pkg.name]:
+ dependlist[pkg.name].add(k)
+ foundlibs.add(k)
+
+ orphans = list(knownlibs - foundlibs)
+ return dependlist, orphans
+
+
+def get_imports(file):
+ root = ast.parse(file.read())
+
+ for node in ast.walk(root):
+ if isinstance(node, ast.Import):
+ for module in node.names:
+ yield module.name.split('.')[0]
+ elif isinstance(node, ast.ImportFrom):
+ if node.module and node.level == 0:
+ yield node.module.split('.')[0]
+
+
+class PythonDependencyRule(TarballRule):
+ name = "pydepends"
+ description = "Checks python dependencies"
+ def analyze(self, pkginfo, tar):
+ liblist = defaultdict(set)
+ own_liblist = set()
+
+ for entry in tar:
+ if not entry.isfile() or not entry.name.endswith('.py'):
+ continue
+ own_liblist.add(entry.name[:-3])
+ f = tar.extractfile(entry)
+ for module in get_imports(f):
+ liblist[module].add(entry.name)
+ f.close()
+
+ for lib in own_liblist:
+ liblist.pop(lib, None)
+
+ dependlist, orphans = finddepends(liblist)
+
+ # Handle "no package associated" errors
+ self.warnings.extend([("library-no-package-associated %s", i)
+ for i in orphans])
+
+ # Print link-level deps
+ for pkg, libraries in depend