File: Synopsis/Parsers/Python/__init__.py
  1#
  2# Copyright (C) 2006 Stefan Seefeld
  3# All rights reserved.
  4# Licensed to the public under the terms of the GNU LGPL (>= 2),
  5# see the file COPYING for details.
  6#
  7
  8from Synopsis.Processor import *
  9from Synopsis import ASG
 10from Synopsis.QualifiedName import QualifiedPythonName as QName
 11from Synopsis.SourceFile import SourceFile
 12from Synopsis.DocString import DocString
 13from ASGTranslator import ASGTranslator
 14from SXRGenerator import SXRGenerator
 15import os
 16
 17__all__ = ['Parser']
 18
 19def expand_package(root, verbose = False):
 20    """Find all modules in a given package."""
 21
 22    modules = []
 23    if not os.path.isdir(root) or not os.path.isfile(os.path.join(root, '__init__.py')):
 24        return modules
 25    if verbose: print 'expanding %s'%root
 26    files = os.listdir(root)
 27    modules += [os.path.join(root, file)
 28                for file in files if os.path.splitext(file)[1] == '.py']
 29    for d in [dir for dir in files if os.path.isdir(os.path.join(root, dir))]:
 30        modules.extend(expand_package(os.path.join(root, d), verbose))
 31    return modules
 32
 33
 34def find_imported(target, base_path, origin, verbose = False):
 35    """
 36    Lookup imported files, based on current file's location.
 37
 38    target: (module, name) pair.
 39    base_path: root directory to which to confine the lookup.
 40    origin: file name of the module issuing the import."""
 41
 42    module, name = target[0].replace('.', os.sep), target[1]
 43    origin = os.path.dirname(origin)
 44    if base_path:
 45        locations = [base_path + origin.rsplit(os.sep, i)[0]
 46                     for i in range(origin.count(os.sep) + 1)] + [base_path]
 47    else:
 48        locations = [origin]
 49
 50    # '*' is only valid in a module scope
 51    if name and name != '*':
 52        # name may be a module or a module's attribute.
 53        # Only find the module.
 54        files = ['%s/%s.py'%(module, name), '%s.py'%module]
 55    else:
 56        files = ['%s.py'%module]
 57    files.append(os.path.join(module, '__init__.py'))
 58    for l in locations:
 59        for f in files:
 60            target = os.path.join(l, f)
 61            if verbose: print 'trying %s'%target
 62            if os.path.exists(target):
 63                return target, target[len(base_path):]
 64    return None, None
 65
 66
 67
 68class Parser(Processor):
 69    """
 70    Python Parser. See http://www.python.org/dev/peps/pep-0258 for additional
 71    info."""
 72
 73    primary_file_only = Parameter(True, 'should only primary file be processed')
 74    base_path = Parameter(None, 'Path prefix to strip off of input file names.')
 75    sxr_prefix = Parameter(None, 'Path prefix (directory) to contain sxr info.')
 76    default_docformat = Parameter('', 'default documentation format')
 77
 78    def process(self, ir, **kwds):
 79
 80        self.set_parameters(kwds)
 81        if not self.input: raise MissingArgument('input')
 82        self.ir = ir
 83        self.scopes = []
 84
 85        # Create return type for Python functions:
 86        self.return_type = ASG.BuiltinTypeId('Python',('',))
 87
 88        # Validate base_path.
 89        if self.base_path:
 90            if not os.path.isdir(self.base_path):
 91                raise InvalidArgument('base_path: "%s" not a directory.'
 92                                      %self.base_path)
 93            if self.base_path[-1] != os.sep:
 94                self.base_path += os.sep
 95
 96        # all_files is a list of (filename, base_path) pairs.
 97        # we have to record the base_path per file, since it defaults to the
 98        # filename itself, for files coming directly from user input.
 99        # For files that are the result of expanded packages it defaults to the
100        # package directory itself.
101        self.all_files = []
102        for i in self.input:
103            base_path = self.base_path or os.path.dirname(i)
104            if base_path and base_path[-1] != os.sep:
105                base_path += os.sep
106            # expand packages into modules
107            if os.path.isdir(i):
108                if os.path.exists(os.path.join(i, '__init__.py')):
109                    self.all_files.extend([(p,base_path)
110                                           for p in expand_package(i, self.verbose)])
111                else:
112                    raise InvalidArgument('"%s" is not a Python package'%i)
113            else:
114                self.all_files.append((i,base_path))
115        # process modules
116        while len(self.all_files):
117            file, base_path = self.all_files.pop()
118            self.process_file(file, base_path)
119
120        return self.output_and_return_ir()
121
122
123    def process_file(self, filename, base_path):
124        """Parse an individual python file."""
125
126        long_filename = filename
127        if filename[:len(base_path)] != base_path:
128            raise InvalidArgument('invalid input filename:\n'
129                                  '"%s" does not match base_path "%s"'
130                                  %(filename, base_path))
131        if self.verbose: print 'parsing %s'%filename
132        short_filename = filename[len(base_path):]
133        sourcefile = SourceFile(short_filename, long_filename, 'Python', True)
134        self.ir.files[short_filename] = sourcefile
135
136        package = None
137        package_name = []
138        package_path = base_path
139        # Only attempt to set up enclosing packages if a base_path was given.
140        if package_path != filename:
141            components = short_filename.split(os.sep)
142            if components[0] == '':
143                package_path += os.sep
144                components = components[1:]
145            for c in components[:-1]:
146                package_name.append(c)
147                package_path = os.path.join(package_path, c)
148                qname = QName(package_name)
149                if not os.path.isfile(os.path.join(package_path, '__init__.py')):
150                    raise InvalidArgument('"%s" is not a package'%qname)
151                # Try to locate the package
152                type_id = self.ir.asg.types.get(qname)
153                if (type_id):
154                    module = type_id.declaration
155                else:
156                    module = ASG.Module(sourcefile, -1, 'package', qname)
157                    self.ir.asg.types[qname] = ASG.DeclaredTypeId('Python', qname, module)
158                    if package:
159                        package.declarations.append(module)
160                    else:
161                        self.ir.asg.declarations.append(module)
162
163                package = module
164
165        translator = ASGTranslator(package, self.ir.asg.types, self.default_docformat)
166        translator.process_file(sourcefile)
167        # At this point, sourcefile contains a single declaration: the module.
168        if package:
169            package.declarations.extend(sourcefile.declarations)
170        else:
171            self.ir.asg.declarations.extend(sourcefile.declarations)
172        if not self.primary_file_only:
173            for i in translator.imports:
174                target = find_imported(i, self.base_path, sourcefile.name, self.verbose)
175                if target[0] and target[1] not in self.ir.files:
176                    # Only process if we have not visited it yet.
177                    self.all_files.append((target[0], base_path))
178
179        if self.sxr_prefix:
180            sxr = os.path.join(self.sxr_prefix, short_filename + '.sxr')
181            dirname = os.path.dirname(sxr)
182            if not os.path.exists(dirname):
183                os.makedirs(dirname, 0755)
184
185            sxr_generator = SXRGenerator()
186            module = sourcefile.declarations[0]
187            sxr_generator.process_file(module.name, sourcefile, sxr)
188