Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Various improvements #18

Open
wants to merge 29 commits into from

3 participants

@jstasiak
  • Refactored and reformatted various bits of code (code style, testability etc.)
  • Added file path formatting option (can print full file paths, take base names only or leave them intact)
  • Implemented few unit tests and configured Travis CI build
@jstasiak

PS. Yes, those changes are backward incompatible (changes few names), notes regarding names etc. are welcome.

@jstasiak

@bos Is there any chance of reviewing/merging this and other pull requests and releasing new code on PyPI in the foreseeable future?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 12, 2013
  1. @jstasiak

    Setup Travis

    jstasiak authored
  2. @jstasiak

    Refactor CodeKey

    jstasiak authored
  3. @jstasiak

    Refactor DisplayFormat

    jstasiak authored
  4. @jstasiak
  5. @jstasiak

    Add some docstrings

    jstasiak authored
  6. @jstasiak

    Fix formatting

    jstasiak authored
  7. @jstasiak
  8. @jstasiak

    Format file paths

    jstasiak authored
  9. @jstasiak
Commits on Apr 8, 2014
  1. @jstasiak
  2. @jstasiak
Commits on Oct 8, 2014
  1. @jstasiak

    Clean up a bit

    jstasiak authored
  2. @jstasiak
  3. @jstasiak

    Prepare for PyPI release

    jstasiak authored
  4. @jstasiak

    Mark package as not zip_safe

    jstasiak authored
  5. @jstasiak

    Generate universal wheels

    jstasiak authored
  6. @jstasiak

    Tune metadata

    jstasiak authored
  7. @jstasiak

    Fix typo

    jstasiak authored
  8. @nvie @jstasiak

    Add ability to run statprof as a module.

    nvie authored jstasiak committed
  9. @nvie @jstasiak

    Expand the script's path first.

    nvie authored jstasiak committed
  10. @nvie @jstasiak
  11. @anntzer @jstasiak

    Use runpy, allows python -mstatprof -mmodule_name.

    anntzer authored jstasiak committed
  12. @jstasiak
  13. @jstasiak

    Update README.rst

    jstasiak authored
  14. @jstasiak

    Bump version to 0.2.0c2

    jstasiak authored
  15. @jstasiak

    Test with Python 3.4

    jstasiak authored
Commits on Oct 9, 2014
  1. @jstasiak
Commits on Dec 16, 2014
  1. @anntzer

    Support python -mstatprof -c cmd; use runpy.run_path.

    anntzer authored
    Support "python -mstatprof -c cmd", mimicking "python -c cmd".  Use
    runpy.run_path for "python -mstatprof scriptpath".
Commits on Apr 17, 2015
  1. @jstasiak

    Release version 0.2.0

    jstasiak authored
This page is out of date. Refresh to see the latest.
View
14 .travis.yml
@@ -0,0 +1,14 @@
+language: python
+python:
+ - "2.6"
+ - "2.7"
+ - "3.2"
+ - "3.3"
+ - "3.4"
+ - "pypy"
+install:
+ - pip install "mock>=1.0.1" "nose>=1.3.0" flake8
+ - python setup.py clean build --build-base="build/$TRAVIS_PYTHON_VERSION" install
+script:
+ - nosetests -v --no-path-adjustment
+ - flake8 --max-line-length=110 *.py
View
5 AUTHORS
@@ -1,5 +1,6 @@
statprof was originally written by Andy Wingo, and later ported to modern
-Python by Alex Frazer. It's now maintained by Bryan O'Sullivan.
+Python by Alex Frazer. Used to be maintained by Bryan O'Sullivan, this fork
+is maintained by Smarkets.
And here is an list of people who have submitted patches, reported bugs, and
generally made statprof that much better:
@@ -9,3 +10,5 @@ generally made statprof that much better:
* Bryan O'Sullivan <bos@serpentine.com>
* Rob Browning <rlb at defaultvalue dot org>
* Timothée Peignier <tim at tryphon dot org>
+ * Vincent Driessen <vincent@3rdcloud.com>
+ * Antony Lee
View
80 README.rst
@@ -1,6 +1,12 @@
statprof - statistical profiling for Python
===========================================
+.. image:: https://travis-ci.org/smarkets/statprof.svg?branch=master
+ :target: https://travis-ci.org/smarkets/statprof
+
+.. image:: https://img.shields.io/pypi/v/statprof-smarkets.svg
+ :target: https://pypi.python.org/pypi/statprof-smarkets
+
This package provides a simple statistical profiler for Python.
Python's default profiler has been ``lsprof`` for several years. This is
@@ -20,27 +26,56 @@ accurately.
implementation and portability notes below for details.
+How to get it
+-------------
+
+Use pip!
+
+::
+
+ pip install statprof-smarkets
+
+Warning: it uses ``statprof`` as Python module name so this will conflict with
+original statprof installation if present.
+
+GitHub project page: https://github.com/smarkets/statprof
+
+PyPI page: https://pypi.python.org/pypi/statprof-smarkets
+
Basic usage
-----------
-It's easy to get started with ``statprof``: ::
+It's easy to get started with ``statprof``:
+
+.. code-block:: python
import statprof
statprof.start()
- try:
- my_questionable_function()
+ try:
+ my_questionable_function()
finally:
- statprof.stop()
- statprof.display()
+ statprof.stop()
+ statprof.display()
+
+Or with a contextmanager:
-Or with a contextmanager : ::
+.. code-block:: python
import statprof
with statprof.profile():
my_questionable_function()
+Or from command line:
+
+::
+
+ $ python -m statprof script.py
+ # or
+ $ python -m statprof -m script
+ # or (this may depend on bash because http://www.gnu.org/software/bash/manual/bashref.html#ANSI_002dC-Quoting)
+ $ python -m statprof -c "import hashlib"$'\n'"for i in range(10000): hashlib.md5(str(i)).hexdigest()"
For more comprehensive help, run ``pydoc statprof``.
@@ -74,24 +109,39 @@ block.
The profiler also tries (as much as possible) to avoid counting or
timing its own code.
+Changelog
+---------
+
+0.2.0
+`````
+
+* forked
+* refactored
+* added configurable display format (displays full paths by default now)
+* ability to run whole scripts under statprof from command line (thanks to
+ `Vincent Driessen <https://github.com/nvie>`_ and
+ `Antony Lee <https://github.com/anntzer>`_
+* added support for ``python -mstatprof -c cmd`` invocation (thanks to
+ `Antony Lee <https://github.com/anntzer>`_)
History
-------
-This package was originally
-written and released by `Andy Wingo <http://wingolog.org/archives/2005/10/28/profiling>`_.
-It was ported to modern Python by Alex Frazer, and posted to github by
-Jeff Muizelaar. The current maintainer is `Bryan O'Sullivan <bos@serpentine.com>`_.
+This package was originally written and released by
+`Andy Wingo <http://wingolog.org/archives/2005/10/28/profiling>`_.
+It was ported to modern Python by Alex Frazer, and posted to GitHub by
+Jeff Muizelaar. Maintained by `Bryan O'Sullivan <bos@serpentine.com>`_, was forked by
+Smarkets due to package not being maintaned anymore.
Reporting bugs, contributing patches
------------------------------------
-The current maintainer of this package is `Bryan O'Sullivan <bos@serpentine.com>`_.
-
-Please report bugs using the `github issue tracker <https://github.com/bos/statprof.py/issues>`_.
+Please report bugs using the `GitHub issue tracker <https://github.com/smarkets/statprof/issues>`_.
If you'd like to contribute patches, please do - the source is on
-github, so please just issue a pull request. ::
+GitHub, so please just issue a pull request.
+
+::
- $ git clone git://github.com/bos/statprof.py
+ $ git clone git://github.com/smarkets/statprof
View
2  setup.cfg
@@ -0,0 +1,2 @@
+[wheel]
+universal = True
View
27 setup.py
@@ -1,24 +1,22 @@
#!/usr/bin/env python
-import os, sys
+import os
+
from setuptools import setup
+
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
-extra = {}
-if sys.version_info >= (3,):
- extra['use_2to3'] = True
-
setup(
- name="statprof",
- version="0.1.3",
- author="Bryan O'Sullivan",
- author_email="bos@serpentine.com",
+ name="statprof-smarkets",
+ version="0.2.0",
+ author="Smarkets",
+ author_email="support@smarkets.com",
description="Statistical profiling for Python",
- license=read('LICENSE'),
- keywords="profiling",
- url="http://packages.python.org/statprof",
+ license='LGPL 2, see LICENSE file',
+ keywords=["profiling", "statistical profiling", "statprof"],
+ url="https://github.com/smarkets/statprof",
py_modules=['statprof'],
long_description=read('README.rst'),
classifiers=[
@@ -29,5 +27,8 @@ def read(fname):
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
],
- **extra
+ install_requires=[
+ 'six>=1.5.0',
+ ],
+ zip_safe=False,
)
View
322 statprof.py
@@ -1,27 +1,30 @@
-## statprof.py
-## Copyright (C) 2012 Bryan O'Sullivan <bos@serpentine.com>
-## Copyright (C) 2011 Alex Fraser <alex at phatcore dot com>
-## Copyright (C) 2004,2005 Andy Wingo <wingo at pobox dot com>
-## Copyright (C) 2001 Rob Browning <rlb at defaultvalue dot org>
-
-## This library is free software; you can redistribute it and/or
-## modify it under the terms of the GNU Lesser General Public
-## License as published by the Free Software Foundation; either
-## version 2.1 of the License, or (at your option) any later version.
-##
-## This library 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
-## Lesser General Public License for more details.
-##
-## You should have received a copy of the GNU Lesser General Public
-## License along with this program; if not, contact:
-##
-## Free Software Foundation Voice: +1-617-542-5942
-## 59 Temple Place - Suite 330 Fax: +1-617-542-2652
-## Boston, MA 02111-1307, USA gnu@gnu.org
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, division, print_function, unicode_literals
"""
+Copyright (C) 2014 Smarkets Limited <support@smarkets.com>
+Copyright (C) 2012 Bryan O'Sullivan <bos@serpentine.com>
+Copyright (C) 2011 Alex Fraser <alex at phatcore dot com>
+Copyright (C) 2004,2005 Andy Wingo <wingo at pobox dot com>
+Copyright (C) 2001 Rob Browning <rlb at defaultvalue dot org>
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library 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
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this program; if not, contact:
+
+Free Software Foundation Voice: +1-617-542-5942
+59 Temple Place - Suite 330 Fax: +1-617-542-2652
+Boston, MA 02111-1307, USA gnu@gnu.org
+
statprof is intended to be a fairly simple statistical profiler for
python. It was ported directly from a statistical profiler for guile,
also named statprof, available from guile-lib [0].
@@ -100,20 +103,23 @@
significantly off if other threads' work patterns are not similar to the
main thread's work patterns.
"""
-from __future__ import division
import os
+import runpy
import signal
+import sys
from collections import defaultdict
from contextlib import contextmanager
+from six import moves, exec_, iteritems, itervalues
-__all__ = ['start', 'stop', 'reset', 'display', 'profile']
+
+__all__ = ['DisplayFormat', 'start', 'stop', 'reset', 'display', 'profile']
###########################################################################
-## Utils
+# Utils
def clock():
times = os.times()
@@ -121,13 +127,18 @@ def clock():
###########################################################################
-## Collection data structures
+# Collection data structures
class ProfileState(object):
def __init__(self, frequency=None):
+ self.profile_level = 0
self.reset(frequency)
+ def is_active(self):
+ return self.profile_level > 0
+
def reset(self, frequency=None):
+ assert self.profile_level == 0, "Can't reset() while statprof is running"
# total so far
self.accumulated_time = 0.0
# start_time when timer is active
@@ -154,6 +165,29 @@ def reset(self, frequency=None):
def accumulate_time(self, stop_time):
self.accumulated_time += stop_time - self.last_start_time
+ def start(self):
+ state.profile_level += 1
+
+ if state.profile_level == 1:
+ self.last_start_time = clock()
+ rpt = self.remaining_prof_time
+ self.remaining_prof_time = None
+ signal.signal(signal.SIGPROF, profile_signal_handler)
+ signal.setitimer(signal.ITIMER_PROF, rpt or self.sample_interval, 0.0)
+ self.gc_time_taken = 0 # dunno
+
+ def stop(self):
+ assert self.profile_level > 0, 'statprof is not running'
+ self.profile_level -= 1
+ if self.profile_level == 0:
+ self.accumulate_time(clock())
+ self.last_start_time = None
+ rpt = signal.setitimer(signal.ITIMER_PROF, 0.0, 0.0)
+ signal.signal(signal.SIGPROF, signal.SIG_IGN)
+ self.remaining_prof_time = rpt[0]
+ self.gc_time_taken = 0 # dunno
+
+
state = ProfileState()
@@ -162,21 +196,32 @@ class CodeKey(object):
__slots__ = ('filename', 'lineno', 'name')
- def __init__(self, frame):
+ def __init__(self, filename, lineno, name):
+ self.filename = filename
+ self.lineno = lineno
+ self.name = name
+
+ @classmethod
+ def create_from_frame(cls, frame):
code = frame.f_code
- self.filename = code.co_filename
- self.lineno = frame.f_lineno
- self.name = code.co_name
+ return cls(code.co_filename, frame.f_lineno, code.co_name)
def __eq__(self, other):
try:
return (self.lineno == other.lineno and
- self.filename == other.filename)
- except:
+ self.filename == other.filename and
+ self.name == other.name)
+ except Exception:
return False
def __hash__(self):
- return hash((self.lineno, self.filename))
+ return hash((self.lineno, self.filename, self.name))
+
+ def __repr__(self):
+ return '%s(%s)' % (
+ self.__class__.__name__,
+ ', '.join('%r' % getattr(self, k) for k in self.__slots__)
+ )
@classmethod
def get(cls, frame):
@@ -184,7 +229,7 @@ def get(cls, frame):
try:
return cls.cache[k]
except KeyError:
- v = cls(frame)
+ v = cls.create_from_frame(frame)
cls.cache[k] = v
return v
@@ -195,6 +240,9 @@ class CallData(object):
__slots__ = ('key', 'call_count', 'cum_sample_count', 'self_sample_count')
def __init__(self, key):
+ '''
+ :type key: :class:`CodeKey`
+ '''
self.key = key
self.call_count = 0
self.cum_sample_count = 0
@@ -211,7 +259,7 @@ def get(cls, key):
###########################################################################
-## SIGPROF handler
+# SIGPROF handler
def sample_stack_procs(frame):
state.sample_count += 1
@@ -231,41 +279,25 @@ def profile_signal_handler(signum, frame):
if state.profile_level > 0:
state.accumulate_time(clock())
sample_stack_procs(frame)
- signal.setitimer(signal.ITIMER_PROF,
- state.sample_interval, 0.0)
+ signal.setitimer(signal.ITIMER_PROF, state.sample_interval, 0.0)
state.last_start_time = clock()
###########################################################################
-## Profiling API
+# Profiling API
def is_active():
- return state.profile_level > 0
+ return state.is_active()
def start():
'''Install the profiling signal handler, and start profiling.'''
- state.profile_level += 1
- if state.profile_level == 1:
- state.last_start_time = clock()
- rpt = state.remaining_prof_time
- state.remaining_prof_time = None
- signal.signal(signal.SIGPROF, profile_signal_handler)
- signal.setitimer(signal.ITIMER_PROF,
- rpt or state.sample_interval, 0.0)
- state.gc_time_taken = 0 # dunno
+ state.start()
def stop():
'''Stop profiling, and uninstall the profiling signal handler.'''
- state.profile_level -= 1
- if state.profile_level == 0:
- state.accumulate_time(clock())
- state.last_start_time = None
- rpt = signal.setitimer(signal.ITIMER_PROF, 0.0, 0.0)
- signal.signal(signal.SIGPROF, signal.SIG_IGN)
- state.remaining_prof_time = rpt[0]
- state.gc_time_taken = 0 # dunno
+ state.stop()
def reset(frequency=None):
@@ -274,10 +306,9 @@ def reset(frequency=None):
The optional frequency argument specifies the number of samples to
collect per second.'''
- assert state.profile_level == 0, "Can't reset() while statprof is running"
+ state.reset(frequency)
CallData.all_calls.clear()
CodeKey.cache.clear()
- state.reset(frequency)
@contextmanager
@@ -291,21 +322,21 @@ def profile():
###########################################################################
-## Reporting API
+# Reporting API
class CallStats(object):
def __init__(self, call_data):
+ '''
+ :type call_data: :class:`CallData`
+ '''
self_samples = call_data.self_sample_count
cum_samples = call_data.cum_sample_count
nsamples = state.sample_count
secs_per_sample = state.accumulated_time / nsamples
- basename = os.path.basename(call_data.key.filename)
self.lineno = call_data.key.lineno
- self.filepath = call_data.key.filename
- self.filename = basename
+ self.filename = call_data.key.filename
self.function = call_data.key.name
- self.name = '%s:%d:%s' % (self.filename, self.lineno, self.function)
self.pcnt_time_in_proc = self_samples / nsamples * 100
self.cum_secs_in_proc = cum_samples * secs_per_sample
self.self_secs_in_proc = self_samples * secs_per_sample
@@ -313,53 +344,79 @@ def __init__(self, call_data):
self.self_secs_per_call = None
self.cum_secs_per_call = None
- def display(self, fp):
- print >> fp, ('%6.2f %9.2f %9.2f %s' % (self.pcnt_time_in_proc,
- self.cum_secs_in_proc,
- self.self_secs_in_proc,
- self.name))
+
+class DisplayFormat:
+ BY_LINE = 0
+ BY_METHOD = 1
+
+
+class PathFormat:
+ FULL_PATH = 0
+ FILENAME_ONLY = 1
+ NO_FORMATTING = 2
-class DisplayFormats:
- ByLine = 0
- ByMethod = 1
+def display(fp=None, format=DisplayFormat.BY_LINE, path_format=PathFormat.FULL_PATH):
+ '''Print statistics, either to stdout or the given file object.
+ :type format: One of :class:`DisplayFormat.BY_*` constants
+ :param all_paths_absolute: Print all the file names with full paths.
+ '''
-def display(fp=None, format=0):
- '''Print statistics, either to stdout or the given file object.'''
+ def p(whatever):
+ print(whatever, file=fp)
if fp is None:
- import sys
fp = sys.stdout
if state.sample_count == 0:
- print >> fp, ('No samples recorded.')
+ p('No samples recorded.')
return
- if format == DisplayFormats.ByLine:
- display_by_line(fp)
- elif format == DisplayFormats.ByMethod:
- display_by_method(fp)
+ stats = [CallStats(x) for x in itervalues(CallData.all_calls)]
+
+ try:
+ path_transformation = {
+ PathFormat.FULL_PATH: os.path.abspath,
+ PathFormat.FILENAME_ONLY: os.path.basename,
+ PathFormat.NO_FORMATTING: lambda path: path
+ }[path_format]
+ except KeyError:
+ raise Exception("Invalid path format")
else:
+ for stat in stats:
+ stat.filename = path_transformation(stat.filename)
+
+ try:
+ method = {
+ DisplayFormat.BY_LINE: display_by_line,
+ DisplayFormat.BY_METHOD: display_by_method
+ }[format]
+ except KeyError:
raise Exception("Invalid display format")
- print >> fp, ('---')
- print >> fp, ('Sample count: %d' % state.sample_count)
- print >> fp, ('Total time: %f seconds' % state.accumulated_time)
+ method(stats, fp)
+
+ p('---')
+ p('Sample count: %d' % state.sample_count)
+ p('Total time: %f seconds' % state.accumulated_time)
-def display_by_line(fp):
+def display_by_line(stats, fp):
'''Print the profiler data with each sample line represented
as one row in a table. Sorted by self-time per line.'''
- l = [CallStats(x) for x in CallData.all_calls.itervalues()]
- l.sort(reverse=True, key=lambda x: x.self_secs_in_proc)
+ stats.sort(reverse=True, key=lambda x: x.self_secs_in_proc)
- print >> fp, ('%5.5s %10.10s %7.7s %-8.8s' %
- ('% ', 'cumulative', 'self', ''))
- print >> fp, ('%5.5s %9.9s %8.8s %-8.8s' %
- ("time", "seconds", "seconds", "name"))
+ def p(whatever):
+ print(whatever, file=fp)
+
+ p('%5.5s %10.10s %7.7s %-8.8s' % ('% ', 'cumulative', 'self', ''))
+ p('%5.5s %9.9s %8.8s %-8.8s' % ("time", "seconds", "seconds", "name"))
+
+ for x in stats:
+ p('%6.2f %9.2f %9.2f %s' % (
+ x.pcnt_time_in_proc, x.cum_secs_in_proc, x.self_secs_in_proc,
+ '%s:%d:%s' % (x.filename, x.lineno, x.function)))
- for x in l:
- x.display(fp)
def get_line_source(filename, lineno):
'''Gets the line text for the line in the file.'''
@@ -378,24 +435,25 @@ def get_line_source(filename, lineno):
return ""
-def display_by_method(fp):
+
+def display_by_method(stats, fp):
'''Print the profiler data with each sample function represented
as one row in a table. Important lines within that function are
output as nested rows. Sorted by self-time per line.'''
- print >> fp, ('%5.5s %10.10s %7.7s %-8.8s' %
- ('% ', 'cumulative', 'self', ''))
- print >> fp, ('%5.5s %9.9s %8.8s %-8.8s' %
- ("time", "seconds", "seconds", "name"))
- calldata = [CallStats(x) for x in CallData.all_calls.itervalues()]
+ def p(whatever):
+ print(whatever, file=fp)
+
+ p('%5.5s %10.10s %7.7s %-8.8s' % ('% ', 'cumulative', 'self', ''))
+ p('%5.5s %9.9s %8.8s %-8.8s' % ("time", "seconds", "seconds", "name"))
grouped = defaultdict(list)
- for call in calldata:
+ for call in stats:
grouped[call.filename + ":" + call.function].append(call)
# compute sums for each function
functiondata = []
- for fname, samples in grouped.iteritems():
+ for fname, samples in iteritems(grouped):
total_cum_sec = 0
total_self_sec = 0
total_percent = 0
@@ -403,8 +461,8 @@ def display_by_method(fp):
total_cum_sec += sample.cum_secs_in_proc
total_self_sec += sample.self_secs_in_proc
total_percent += sample.pcnt_time_in_proc
- functiondata.append((fname,
- total_cum_sec,
+ functiondata.append((fname,
+ total_cum_sec,
total_self_sec,
total_percent,
samples))
@@ -413,19 +471,63 @@ def display_by_method(fp):
functiondata.sort(reverse=True, key=lambda x: x[2])
for function in functiondata:
- print >> fp, ('%6.2f %9.2f %9.2f %s' % (function[3], # total percent
- function[1], # total cum sec
- function[2], # total self sec
- function[0])) # file:function
+ p('%6.2f %9.2f %9.2f %s' % (
+ function[3], # total percent
+ function[1], # total cum sec
+ function[2], # total self sec
+ function[0])) # file:function
function[4].sort(reverse=True, key=lambda i: i.self_secs_in_proc)
for call in function[4]:
# only show line numbers for significant locations ( > 1% time spent)
if call.pcnt_time_in_proc > 1:
- source = get_line_source(call.filepath, call.lineno).strip()
+ source = get_line_source(call.filename, call.lineno).strip()
if len(source) > 25:
source = source[:20] + "..."
- print >> fp, ('%33.0f%% %6.2f line %s: %s' % (call.pcnt_time_in_proc,
- call.self_secs_in_proc,
- call.lineno,
- source))
+ p('%33.0f%% %6.2f line %s: %s' % (
+ call.pcnt_time_in_proc,
+ call.self_secs_in_proc,
+ call.lineno,
+ source))
+
+
+def main():
+ '''Run the given script under the profiler, when invoked as a module
+ (python -m statprof ...), and display the profile report once done.
+ '''
+ if not sys.argv[1:] or sys.argv[1] in ('--help', '-h'):
+ print('usage: python -m statprof [-c cmd | -m mod | file] [<args>]')
+ sys.exit(2)
+
+ scriptfile = sys.argv[1]
+
+ if scriptfile.startswith("-c"):
+ del sys.argv[0] # Hide 'statprof' from argument list
+ if scriptfile == "-c":
+ scriptfile = sys.argv[1]
+ del sys.argv[1]
+ else:
+ scriptfile = scriptfile[2:]
+ sys.argv[0] = "-c"
+ with profile():
+ exec_(scriptfile, vars(moves.builtins))
+
+ elif scriptfile.startswith("-m"):
+ if scriptfile == "-m":
+ scriptfile = sys.argv[2]
+ del sys.argv[1:3]
+ else:
+ scriptfile = scriptfile[2:]
+ del sys.argv[1]
+ with profile():
+ runpy.run_module(scriptfile, run_name="__main__", alter_sys=True)
+
+ else:
+ del sys.argv[0] # Hide 'statprof' from argument list
+ with profile():
+ runpy.run_path(scriptfile, run_name="__main__")
+
+
+if __name__ == '__main__':
+ import statprof
+ statprof.main()
View
116 test_statprof.py
@@ -0,0 +1,116 @@
+try:
+ from cStringIO import StringIO
+except ImportError:
+ try:
+ from StringIO import StringIO
+ except ImportError:
+ from io import StringIO # noqa
+
+from os.path import abspath, basename
+
+from mock import Mock
+from nose.tools import eq_, ok_
+
+from statprof import CodeKey, display, DisplayFormat, PathFormat, ProfileState, start, stop
+
+
+def create_frame(filename, lineno, name):
+ code = Mock()
+ code.co_name = name
+ code.co_filename = filename
+
+ frame = Mock()
+ frame.f_code = code
+ frame.f_lineno = lineno
+
+ return frame
+
+
+def get_output(**kwargs):
+ buffer = StringIO()
+ display(buffer, **kwargs)
+ buffer.seek(0)
+ return buffer.read()
+
+
+def neq(a, b, msg=None):
+ if not a != b:
+ raise AssertionError(msg or "%r == %r" % (a, b))
+
+
+def is_(a, b, msg=None):
+ if a is not b:
+ raise AssertionError(msg or "%r is not %r" % (a, b))
+
+
+def test_code_key_equitability_and_hashability():
+ ck = CodeKey('test.py', 11, 'test')
+ eq_(ck, ck)
+
+ ok_(ck in (ck,))
+ ok_(ck in [ck])
+ ok_(ck in set((ck,)))
+
+ eq_(CodeKey('test.py', 22, 'test'), CodeKey('test.py', 22, 'test'))
+
+ neq(CodeKey('test.py', 22, 'test'), CodeKey('some_other_test.py', 22, 'test'))
+ neq(CodeKey('test.py', 22, 'test'), CodeKey('test.py', 123, 'test'))
+
+
+def test_code_key_create_from_frame_generates_correct_result():
+ args = ('/tmp/test.py', 123, 'do_something')
+ frame = create_frame(*args)
+ code_key = CodeKey.create_from_frame(frame)
+ eq_(
+ (code_key.filename, code_key.lineno, code_key.name),
+ args
+ )
+
+
+def test_code_key_get_method_reuses_instances():
+ args = 'main.py', 11, 'main'
+ frame = create_frame(*args)
+ ck1 = CodeKey.get(frame)
+ ck2 = CodeKey.get(frame)
+
+ is_(ck2, ck1)
+ eq_(
+ (ck1.filename, ck1.lineno, ck1.name),
+ args
+ )
+
+
+def test_stop_fails_if_profiling_isnt_running():
+ state = ProfileState()
+ try:
+ state.stop()
+ except Exception:
+ pass
+ else:
+ raise Exception('Call above should have failed')
+
+
+def test_profiling_output_contains_file_names_formatted_appropriately():
+ start()
+
+ def fun():
+ for i in range(2 ** 20):
+ pass
+
+ def fun2():
+ for i in range(50):
+ fun()
+
+ fun2()
+ stop()
+
+ for format in (DisplayFormat.BY_LINE, DisplayFormat.BY_METHOD):
+ full_path = abspath(__file__)
+ base = basename(__file__)
+
+ content = get_output(format=format, path_format=PathFormat.FULL_PATH)
+ assert full_path in content
+
+ content = get_output(format=format, path_format=PathFormat.FILENAME_ONLY)
+ assert base in content
+ assert full_path not in content
Something went wrong with that request. Please try again.