diff options
Diffstat (limited to 'coverage/codeunit.py')
-rw-r--r-- | coverage/codeunit.py | 213 |
1 files changed, 190 insertions, 23 deletions
diff --git a/coverage/codeunit.py b/coverage/codeunit.py index c58e237b..c9ab2622 100644 --- a/coverage/codeunit.py +++ b/coverage/codeunit.py @@ -1,18 +1,24 @@ """Code unit (module) handling for Coverage.""" -import glob, os +import os -from coverage.backward import open_source, string_class, StringIO -from coverage.misc import CoverageException +from coverage.backward import open_python_source, string_class +from coverage.misc import CoverageException, NoSource +from coverage.parser import CodeParser, PythonParser +from coverage.phystokens import source_token_lines, source_encoding -def code_unit_factory(morfs, file_locator): +def code_unit_factory(morfs, file_locator, get_plugin=None): """Construct a list of CodeUnits from polymorphic inputs. `morfs` is a module or a filename, or a list of same. `file_locator` is a FileLocator that can help resolve filenames. + `get_plugin` is a function taking a filename, and returning a plugin + responsible for the file. It can also return None if there is no plugin + claiming the file. + Returns a list of CodeUnit objects. """ @@ -20,16 +26,30 @@ def code_unit_factory(morfs, file_locator): if not isinstance(morfs, (list, tuple)): morfs = [morfs] - # On Windows, the shell doesn't expand wildcards. Do it here. - globbed = [] + code_units = [] for morf in morfs: - if isinstance(morf, string_class) and ('?' in morf or '*' in morf): - globbed.extend(glob.glob(morf)) + plugin = None + if isinstance(morf, string_class) and get_plugin: + plugin = get_plugin(morf) + if plugin: + klass = plugin.code_unit_class(morf) + #klass = DjangoTracer # NOT REALLY! TODO + # Hacked-in Mako support. Define COVERAGE_MAKO_PATH as a fragment of + # the path that indicates the Python file is actually a compiled Mako + # template. THIS IS TEMPORARY! + #MAKO_PATH = os.environ.get('COVERAGE_MAKO_PATH') + #if MAKO_PATH and isinstance(morf, string_class) and MAKO_PATH in morf: + # # Super hack! Do mako both ways! + # if 0: + # cu = PythonCodeUnit(morf, file_locator) + # cu.name += '_fako' + # code_units.append(cu) + # klass = MakoCodeUnit + #elif isinstance(morf, string_class) and morf.endswith(".html"): + # klass = DjangoCodeUnit else: - globbed.append(morf) - morfs = globbed - - code_units = [CodeUnit(morf, file_locator) for morf in morfs] + klass = PythonCodeUnit + code_units.append(klass(morf, file_locator)) return code_units @@ -44,6 +64,7 @@ class CodeUnit(object): `relative` is a boolean. """ + def __init__(self, morf, file_locator): self.file_locator = file_locator @@ -51,11 +72,7 @@ class CodeUnit(object): f = morf.__file__ else: f = morf - # .pyc files should always refer to a .py instead. - if f.endswith(('.pyc', '.pyo')): - f = f[:-1] - elif f.endswith('$py.class'): # Jython - f = f[:-9] + ".py" + f = self._adjust_filename(f) self.filename = self.file_locator.canonical_filename(f) if hasattr(morf, '__name__'): @@ -73,9 +90,15 @@ class CodeUnit(object): self.name = n self.modname = modname + self._source = None + def __repr__(self): return "<CodeUnit name=%r filename=%r>" % (self.name, self.filename) + def _adjust_filename(self, f): + # TODO: This shouldn't be in the base class, right? + return f + # Annoying comparison operators. Py3k wants __lt__ etc, and Py2k needs all # of them defined. @@ -99,7 +122,7 @@ class CodeUnit(object): the same directory, but need to differentiate same-named files from different directories. - For example, the file a/b/c.py might return 'a_b_c' + For example, the file a/b/c.py will return 'a_b_c' """ if self.modname: @@ -108,26 +131,105 @@ class CodeUnit(object): root = os.path.splitdrive(self.name)[1] return root.replace('\\', '_').replace('/', '_').replace('.', '_') - def source_file(self): - """Return an open file for reading the source of the code unit.""" + def source(self): + if self._source is None: + self._source = self.get_source() + return self._source + + def get_source(self): + """Return the source code, as a string.""" if os.path.exists(self.filename): # A regular text file: open it. - return open_source(self.filename) + with open_python_source(self.filename) as f: + return f.read() # Maybe it's in a zip file? source = self.file_locator.get_zip_data(self.filename) if source is not None: - return StringIO(source) + return source # Couldn't find source. raise CoverageException( "No source for code '%s'." % self.filename ) + def source_token_lines(self): + """Return the 'tokenized' text for the code.""" + for line in self.source().splitlines(): + yield [('txt', line)] + def should_be_python(self): """Does it seem like this file should contain Python? - This is used to decide if a file reported as part of the exection of + This is used to decide if a file reported as part of the execution of + a program was really likely to have contained Python in the first + place. + """ + return False + + def get_parser(self, exclude=None): + raise NotImplementedError + + +class PythonCodeUnit(CodeUnit): + """Represents a Python file.""" + + def _adjust_filename(self, fname): + # .pyc files should always refer to a .py instead. + if fname.endswith(('.pyc', '.pyo')): + fname = fname[:-1] + elif fname.endswith('$py.class'): # Jython + fname = fname[:-9] + ".py" + return fname + + def get_parser(self, exclude=None): + actual_filename, source = self._find_source(self.filename) + return PythonParser( + text=source, filename=actual_filename, exclude=exclude, + ) + + def _find_source(self, filename): + """Find the source for `filename`. + + Returns two values: the actual filename, and the source. + + The source returned depends on which of these cases holds: + + * The filename seems to be a non-source file: returns None + + * The filename is a source file, and actually exists: returns None. + + * The filename is a source file, and is in a zip file or egg: + returns the source. + + * The filename is a source file, but couldn't be found: raises + `NoSource`. + + """ + source = None + + base, ext = os.path.splitext(filename) + TRY_EXTS = { + '.py': ['.py', '.pyw'], + '.pyw': ['.pyw'], + } + try_exts = TRY_EXTS.get(ext) + if not try_exts: + return filename, None + + for try_ext in try_exts: + try_filename = base + try_ext + if os.path.exists(try_filename): + return try_filename, None + source = self.file_locator.get_zip_data(try_filename) + if source: + return try_filename, source + raise NoSource("No source for code: '%s'" % filename) + + def should_be_python(self): + """Does it seem like this file should contain Python? + + This is used to decide if a file reported as part of the execution of a program was really likely to have contained Python in the first place. @@ -143,3 +245,68 @@ class CodeUnit(object): return True # Everything else is probably not Python. return False + + def source_token_lines(self): + return source_token_lines(self.source()) + + def source_encoding(self): + return source_encoding(self.source()) + + +class MakoParser(CodeParser): + def __init__(self, metadata): + self.metadata = metadata + + def parse_source(self): + """Returns executable_line_numbers, excluded_line_numbers""" + executable = set(self.metadata['line_map'].values()) + return executable, set() + + def translate_lines(self, lines): + tlines = set() + for l in lines: + try: + tlines.add(self.metadata['full_line_map'][l]) + except IndexError: + pass + return tlines + + +class MakoCodeUnit(CodeUnit): + def __init__(self, *args, **kwargs): + super(MakoCodeUnit, self).__init__(*args, **kwargs) + from mako.template import ModuleInfo + py_source = open(self.filename).read() + self.metadata = ModuleInfo.get_module_source_metadata(py_source, full_line_map=True) + + def get_source(self): + return open(self.metadata['filename']).read() + + def get_parser(self, exclude=None): + return MakoParser(self.metadata) + + def source_encoding(self): + return self.metadata['source_encoding'] + + +class DjangoCodeUnit(CodeUnit): + def get_source(self): + with open(self.filename) as f: + return f.read() + + def get_parser(self, exclude=None): + return DjangoParser(self.filename) + + def source_encoding(self): + return "utf8" + + +class DjangoParser(CodeParser): + def __init__(self, filename): + self.filename = filename + + def parse_source(self): + with open(self.filename) as f: + source = f.read() + executable = set(range(1, len(source.splitlines())+1)) + return executable, set() |