[Zope3-checkins] CVS: Zope3/utilities - finddeps.py:1.1

Stephan Richter srichter at cosmos.phy.tufts.edu
Wed Feb 25 10:29:21 EST 2004


Update of /cvs-repository/Zope3/utilities
In directory cvs.zope.org:/tmp/cvs-serv3983/utilities

Added Files:
	finddeps.py 
Log Message:
First attempt to develop a tool to find dependencies. The usage is 
documented in the module doc string.


=== Added File Zope3/utilities/finddeps.py ===
##############################################################################
#
# Copyright (c) 2004 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Script to determine the dependencies of a package or module

This script walks through the modules of a package or only observes a
file-based module to determine its dependencies. 

Usage: finddeps.py [options]
Options:
    -a / --a
        Find all dependencies. This means that the program will also scan the
        dependencies originally found in the module.

    -h / --help
        Print this message and exit.

    -l / --long
        If long is specified, the file and line where the dependency occurs is
        reported.

    -m / --module
        Specify the path of the module that is being inspected.

    -z / --zcml
        Also look through ZCML files for dependencies.

Important: Make sure that the PYTHONPATH is set to or includes 'ZOPE3/src'.

$Id: finddeps.py,v 1.1 2004/02/25 15:29:20 srichter Exp $
"""
import sys
import getopt
import os
import re

# Get the Zope base path
import zope
ZOPESRC=os.path.split(os.path.split(zope.__file__)[0])[0]

# Matching expression for python files.
pythonfile = re.compile(r'[a-zA-Z][a-zA-Z0-9_]*\.py')
zcmlfile = re.compile(r'[a-zA-Z][a-zA-Z0-9_]*\.zcml')

# Matching expressions of dotted paths in XML
dottedName = re.compile(r'"[a-zA-Z\.][a-zA-Z0-9_\.]*"')


class Dependency(object):
    """Object representing a dependency."""

    def __init__(self, path, file, lineno):
        self.path = path
        self.occurences = [(file, lineno)] 

    def addOccurence(self, file, lineno):
        """Add occurenace of the dependency in the code."""
        self.occurences.append((file, lineno))

    def isSubPackageOf(self, dep):
        """Check wether this dependency's path is represents a sub-package of
        dep's path."""
        path = self.path.split('.')
        deppath = dep.path.split('.')
        for i in range(len(path)):
            if i >= len(deppath):
                return True
            if path[i] != deppath[i]:
                break
        return False

    def __cmp__(self, other):
        """Compare dependecies by path."""
        return cmp(self.path, other.path)
    

def usage(code, msg=''):
    """Display help."""
    print >> sys.stderr, '\n'.join(__doc__.split('\n')[:-2])
    if msg:
        print >> sys.stderr, '** Error: ' + str(msg) + ' **'
    sys.exit(code)


def makePythonPath(path):
    """Make out of a patha  dotted Python path, using sys.path"""
    syspaths = sys.path[1:]
    syspaths.append(os.getcwd())
    for syspath in syspaths:
        if path.startswith(syspath):
            cutPath = path.replace(syspath+'/', '')
            dottedPath = '.'.join(cutPath.split('/'))
            return dottedPath

    raise ValueError, 'Cannot create dotted path.'
    

def getDependenciesOfPythonFile(path):
    """Look through a file for dependencies."""
    deps = []
    lineno = 0
    for line in open(path, 'r').readlines():
        lineno += 1
        if line.startswith('from') or line.startswith('import'):
            deps.append(Dependency(line.split(' ')[1].strip(), path, lineno))
    return deps


def getDependenciesOfZCMLFile(path):
    """Get dependencies from ZCML file."""
    localModule = os.path.split(path)[0].replace(ZOPESRC+'/', '')
    localModule = localModule.replace('/', '.')
    deps = []
    lineno = 0
    for line in open(path, 'r').readlines():
        lineno += 1
        match = dottedName.findall(line)
        if match:
            match[0] = match[0][1:-1]
            match.append('.'.join(match[0].split('.')[:-1]))

            # zope and zope.app shoudl never be dependencies; they are too general
            if 'zope' in match:
                match.remove('zope')
            if 'zope.app' in match:
                match.remove('zope.app')                

            for name in match:
                if name.startswith('.'):
                    name = localModule + name
                try:
                    __import__(name)
                except:
                    continue
                deps.append(Dependency(name, path, lineno)) 
    return deps


def filterStandardModules(deps):
    """Try to remove modules that are part of the standard Python library.

    Note: we consider zope and zope.app to be standard modules, since they are
    basically containers.
    """
    filteredDeps = []
    for dep in deps:    
        try:
            module = __import__(dep.path)
        except ImportError:
            continue
        # built-ins (like sys) do not have a file associated
        if not hasattr(module, '__file__'):
            continue
        dir = os.path.split(module.__file__)[0]
        if dir.startswith(ZOPESRC) and dep.path not in ('zope', 'zope.app'):
            filteredDeps.append(dep)
    return filteredDeps


def filterLocalModules(deps, path):
    """Filter out local module imports."""
    # File-based modules cannot have relative imports
    if os.path.isfile(path):
        return deps

    # Filter relative imports
    filteredDeps = []
    for dep in deps:
        module = dep.path.split('.')[0]
        modulePath = os.path.join(path, module)
        if not (os.path.exists(modulePath) or os.path.exists(modulePath+'.py')):
            filteredDeps.append(dep)
    deps = filteredDeps

    # Filter absolute imports
    dottedPath = makePythonPath(path)
    filteredDeps = []
    for dep in deps:
        if not dep.path.startswith(dottedPath):
            filteredDeps.append(dep)

    return filteredDeps


def filterMostGeneral(deps):
    """Return only the parent module and no children.

    for example (foo, foo.bar) --> (foo,)
    """
    newdeps = []
    for dep in deps:
        subpackage = False
        for parentdep in deps:
            if parentdep is not dep and dep.isSubPackageOf(parentdep):
                subpackage = True
                break
        if not subpackage:
            newdeps.append(dep)
    return newdeps


def makeUnique(deps):
    """Remove entries that appear multiple times"""
    uniqueDeps = {}
    for dep in deps:
        if not dep.path in uniqueDeps.keys():
            uniqueDeps[dep.path] = dep
        else:
            uniqueDeps[dep.path].addOccurence(*dep.occurences[0])

    return uniqueDeps.values()


def getDependencies(path, zcml=False):
    """Get all dependencies of a package or module.

    If the path is a package, all Python source files are searched inside it.
    """
    if os.path.isdir(path):
        deps = []
        for file in os.listdir(path):
            filePath = os.path.join(path, file)
            if pythonfile.match(file):
                deps += getDependenciesOfPythonFile(filePath)
            elif zcml and zcmlfile.match(file):
                deps += getDependenciesOfZCMLFile(filePath)                
            elif os.path.isdir(filePath) and \
                     '__init__.py' in os.listdir(filePath):
                deps += getDependencies(filePath)

    elif os.path.isfile(path):
        deps = getDependenciesOfFile(path)

    return deps


def getCleanedDependencies(path, zcml=False):
    """Return clean dependency list."""
    deps = getDependencies(path, zcml)
    deps = filterStandardModules(deps)
    deps = filterLocalModules(deps, path)
    deps = filterMostGeneral(deps)
    deps = makeUnique(deps)
    deps.sort()
    return deps


def getAllCleanedDependencies(path, zcml=False, deps=None, paths=None):
    """Return a list of all cleaned dependencies in a path."""
    # zope and zope/app are too general to be considered.
    if path.endswith('src/zope/') or path.endswith('src/zope/app/'):
        return deps
    
    if deps is None:
        deps = []
        paths = []

    newdeps = getCleanedDependencies(path)
    for dep in newdeps:
        if dep.path not in paths:
            deps.append(dep)
            paths.append(dep.path)
            modulePath = __import__(dep.path).__file__
            if modulePath.endswith('__init__.pyc'):
                modulePath = modulePath[:-12]
            getAllCleanedDependencies(modulePath, zcml, deps, paths)
    deps = filterMostGeneral(deps)
    deps.sort()
    return deps


def showDependencies(path, zcml=False, long=False, all=False):
    """Show the dependencies of a module on the screen."""
    if all:
        deps = getAllCleanedDependencies(path, zcml)
    else:
        deps = getCleanedDependencies(path, zcml)

    print '='*(8+len(path))
    print "Module: " + path
    print '='*(8+len(path))
    for dep in deps:
        print dep.path
        if long:
            print '-'*len(dep.path)
            for file, lineno in dep.occurences:
                file = file.replace(ZOPESRC+'/', '')
                if len(file) >= 69:
                    file = '...' + file[:69-3]
                print '  %s, Line %s' %(file, lineno)
            print


if __name__ == '__main__':
    try:
        opts, args = getopt.getopt(
            sys.argv[1:],
            'm:ahlz',
            ['all', 'help', 'module=', 'long', 'zcml'])
    except getopt.error, msg:
        usage(1, msg)

    all = False
    long = False
    module = None
    zcml = False
    for opt, arg in opts:
        if opt in ('-a', '--all'):
            all = True
        elif opt in ('-h', '--help'):
            usage(0)
        elif opt in ('-l', '--long'):
            long = True
        elif opt in ('-m', '--module'):
            cwd = os.getcwd()
            # This is for symlinks. Thanks to Fred for this trick.
            if os.environ.has_key('PWD'):
                cwd = os.environ['PWD']
            module = os.path.normpath(os.path.join(cwd, arg))
        elif opt in ('-z', '--zcml'):
            zcml = True

    if module is None:
        usage(1, 'The module must be specified.')
    showDependencies(module, zcml, long, all)




More information about the Zope3-Checkins mailing list