diff --git a/Main.sublime-menu b/Main.sublime-menu new file mode 100644 index 0000000..7657df1 --- /dev/null +++ b/Main.sublime-menu @@ -0,0 +1,35 @@ +[ + { + "caption": "Preferences", + "children": [ + { + "caption": "Package Settings", + "children": [ + { + "caption": "Python Coverage", + "children": [ + { + "args": { + "file": "${packages}/Python Coverage/Preferences.sublime-settings" + }, + "caption": "Settings \u2013 Default", + "command": "open_file" + }, + { + "args": { + "file": "${packages}/User/Python Coverage.sublime-settings" + }, + "caption": "Settings \u2013 User", + "command": "open_file" + } + ] + } + ], + "id": "package-settings", + "mnemonic": "P" + } + ], + "id": "preferences", + "mnemonic": "n" + } +] diff --git a/Preferences.sublime-settings b/Preferences.sublime-settings new file mode 100644 index 0000000..3797639 --- /dev/null +++ b/Preferences.sublime-settings @@ -0,0 +1,4 @@ +{ + "use_shell": false, // If true, use system Python rather than Sublime's built-in Python. + "python_path" : "" // Determines Python path if `use_shell` is true. If empty, defaults to best guess. +} diff --git a/SublimePythonCoverage.py b/SublimePythonCoverage.py index c38afe2..72f7ace 100644 --- a/SublimePythonCoverage.py +++ b/SublimePythonCoverage.py @@ -30,44 +30,10 @@ import sublime import sublime_plugin -from coverage import coverage -from coverage.files import FnmatchMatcher +from utils import get_missing_coverage_lines, find, find_cmd, find_tests PLUGIN_FILE = os.path.abspath(__file__) -def find(base, rel, access=os.R_OK): - if not isinstance(rel, basestring): - rel = os.path.join(*rel) - while 1: - path = os.path.join(base, rel) - if os.access(path, access): - return path - baseprev = base - base = os.path.dirname(base) - if not base or base == baseprev: - return - - -def find_cmd(base, cmd): - return find(base, ('bin', cmd), os.X_OK) - - -def find_tests(fname): - dirname = os.path.dirname(fname) - init = os.path.join(dirname, '__init__.py') - if not os.path.exists(init): - # not a package; run tests for the file - return fname - - setup = find(dirname, 'setup.py') - if setup: - # run tests for the whole distribution - return os.path.dirname(setup) - - # run tests for the package - return os.path.dirname(fname) - - class SublimePythonCoverageListener(sublime_plugin.EventListener): """Event listener to highlight uncovered lines when a Python file is loaded.""" @@ -82,6 +48,15 @@ class ShowPythonCoverageCommand(sublime_plugin.TextCommand): """Highlight uncovered lines in the current file based on a previous coverage run.""" def run(self, edit): + def get_setting(key, default): + return user_settings.get(key, settings.get(key, default)) + + user_settings = sublime.load_settings('Python Coverage.sublime-settings') + settings = sublime.load_settings('Preferences.sublime-settings') + + use_shell = get_setting('use_shell', False) + python_path = get_setting('python_path', '') + view = self.view fname = view.file_name() if not fname: @@ -92,18 +67,13 @@ def run(self, edit): print 'Could not find .coverage file.' return - # run analysis and find uncovered lines - cov = coverage(data_file=cov_file) outlines = [] - omit_matcher = FnmatchMatcher(cov.omit) - if not omit_matcher.match(fname): - cov_dir = os.path.dirname(cov_file) - os.chdir(cov_dir) - relpath = os.path.relpath(fname, cov_dir) - cov.load() - f, s, excluded, missing, m = cov.analysis2(relpath) - for line in missing: - outlines.append(view.full_line(view.text_point(line - 1, 0))) + missing_coverage_lines = get_missing_coverage_lines( + fname, cov_file, PLUGIN_FILE, + quiet=False, use_shell=use_shell, python_path=python_path + ) + for line in missing_coverage_lines: + outlines.append(view.full_line(view.text_point(line - 1, 0))) # update highlighted regions view.erase_regions('SublimePythonCoverage') @@ -111,6 +81,8 @@ def run(self, edit): view.add_regions('SublimePythonCoverage', outlines, 'markup.inserted', 'bookmark', sublime.HIDDEN) + + # manually import the module containing ST2's default build command, # since it's in a module whose name is a Python keyword :-s ExecCommand = __import__('exec').ExecCommand diff --git a/run_gmcl.py b/run_gmcl.py new file mode 100644 index 0000000..a58f1a2 --- /dev/null +++ b/run_gmcl.py @@ -0,0 +1,19 @@ +# Print the result of get_missing_coverage_lines() +# +# Usage: +# python run_gmcl.py FILENAME COVERAGEFILENAME + + +from utils import get_missing_coverage_lines +import os.path +import sys + + +def main(): + print "\n".join(map(str, get_missing_coverage_lines( + *sys.argv[1:] + [os.path.abspath(__file__)] + ))) + + +if __name__ == '__main__': + main() diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..15d5907 --- /dev/null +++ b/utils.py @@ -0,0 +1,79 @@ +from coverage import coverage +from coverage.files import FnmatchMatcher +import os +import os.path +import subprocess + + +def get_missing_coverage_lines(fname, cov_file, plugin_file, quiet=True, use_shell=False, python_path=None): + def find_python_path(): + for path in os.environ['PATH'].split(os.pathsep): + filename = os.path.join(path, 'python') + if os.path.isfile(filename): + return filename + + if use_shell: + if not python_path: + python_path = find_python_path() + + if not quiet: + print 'Running coverage via shell (Python=%r)' % python_path + + cmd = [ + python_path, os.path.abspath(os.path.join(os.path.dirname(plugin_file), 'run_gmcl.py')), + fname, cov_file + ] + + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (stdout, stderr) = proc.communicate() + if proc.returncode != 0: + raise Exception('%r failed with returncode %s. Stderr:\n%s' % (cmd, proc.returncode, stderr)) + + return map(int, stdout.split()) + else: + if not quiet: + print 'Running coverage via Sublime Python' + + cov = coverage(data_file=cov_file) + omit_matcher = FnmatchMatcher(cov.omit) + if not omit_matcher.match(fname): + cov_dir = os.path.dirname(cov_file) + os.chdir(cov_dir) + relpath = os.path.relpath(fname, cov_dir) + cov.load() + f, s, excluded, missing, m = cov.analysis2(relpath) + return missing + return [] + + +def find(base, rel, access=os.R_OK): + if not isinstance(rel, basestring): + rel = os.path.join(*rel) + while 1: + path = os.path.join(base, rel) + if os.access(path, access): + return path + baseprev = base + base = os.path.dirname(base) + if not base or base == baseprev: + return + + +def find_cmd(base, cmd): + return find(base, ('bin', cmd), os.X_OK) + + +def find_tests(fname): + dirname = os.path.dirname(fname) + init = os.path.join(dirname, '__init__.py') + if not os.path.exists(init): + # not a package; run tests for the file + return fname + + setup = find(dirname, 'setup.py') + if setup: + # run tests for the whole distribution + return os.path.dirname(setup) + + # run tests for the package + return os.path.dirname(fname)