diff options
author | noptuno <repollo.marrero@gmail.com> | 2023-04-28 02:29:30 +0200 |
---|---|---|
committer | noptuno <repollo.marrero@gmail.com> | 2023-04-28 02:29:30 +0200 |
commit | 355dee533bb34a571b9367820a63cccb668cf866 (patch) | |
tree | 838af886b4fec07320aeb10f0d1e74ba79e79b5c /venv/lib/python3.9/site-packages/pympler | |
parent | added pyproject.toml file (diff) | |
download | gpt4free-355dee533bb34a571b9367820a63cccb668cf866.tar gpt4free-355dee533bb34a571b9367820a63cccb668cf866.tar.gz gpt4free-355dee533bb34a571b9367820a63cccb668cf866.tar.bz2 gpt4free-355dee533bb34a571b9367820a63cccb668cf866.tar.lz gpt4free-355dee533bb34a571b9367820a63cccb668cf866.tar.xz gpt4free-355dee533bb34a571b9367820a63cccb668cf866.tar.zst gpt4free-355dee533bb34a571b9367820a63cccb668cf866.zip |
Diffstat (limited to 'venv/lib/python3.9/site-packages/pympler')
36 files changed, 12255 insertions, 0 deletions
diff --git a/venv/lib/python3.9/site-packages/pympler/__init__.py b/venv/lib/python3.9/site-packages/pympler/__init__.py new file mode 100644 index 00000000..cd7ca498 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/__init__.py @@ -0,0 +1 @@ +__version__ = '1.0.1' diff --git a/venv/lib/python3.9/site-packages/pympler/asizeof.py b/venv/lib/python3.9/site-packages/pympler/asizeof.py new file mode 100644 index 00000000..f9e87c96 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/asizeof.py @@ -0,0 +1,2840 @@ +#!/usr/bin/env python + +# Copyright, license and disclaimer are at the very end of this file. + +# This is the latest, enhanced version of the asizeof.py recipes at +# <http://ASPN.ActiveState.com/ASPN/Cookbook/Python/Recipe/546530> +# <http://ASPN.ActiveState.com/ASPN/Cookbook/Python/Recipe/544288> + +# Note, objects like ``namedtuples``, ``closure``, and NumPy data +# ``arange``, ``array``, ``matrix``, etc. are only handled by recent +# versions of this module. Sizing of ``__slots__`` has been incorrect +# in versions before this one. Also, property ``Asizer.duplicate`` gave +# incorrect values before this release. Several other properties +# have been added to the ``Asizer`` class and the ``print_summary`` +# method has been updated. + +''' +This module exposes 9 functions and 2 classes to obtain lengths and +sizes of Python objects (for Python 3.5 or later). + +Earlier versions of this module supported Python versions down to +Python 2.2. If you are using Python 3.5 or older, please consider +downgrading Pympler. + +**Public Functions** [#unsafe]_ + + Function **asizeof** calculates the combined (approximate) size + in bytes of one or several Python objects. + + Function **asizesof** returns a tuple containing the (approximate) + size in bytes for each given Python object separately. + + Function **asized** returns for each object an instance of class + **Asized** containing all the size information of the object and + a tuple with the referents [#refs]_. + + Functions **basicsize** and **itemsize** return the *basic-* + respectively *itemsize* of the given object, both in bytes. For + objects as ``array.array``, ``numpy.array``, ``numpy.matrix``, + etc. where the item size varies depending on the instance-specific + data type, function **itemsize** returns that item size. + + Function **flatsize** returns the *flat size* of a Python object + in bytes defined as the *basic size* plus the *item size* times + the *length* of the given object. + + Function **leng** returns the *length* of an object, like standard + function ``len`` but extended for several types. E.g. the **leng** + of a multi-precision int (or long) is the number of ``digits`` + [#digit]_. The length of most *mutable* sequence objects includes + an estimate of the over-allocation and therefore, the **leng** value + may differ from the standard ``len`` result. For objects like + ``array.array``, ``numpy.array``, ``numpy.matrix``, etc. function + **leng** returns the proper number of items. + + Function **refs** returns (a generator for) the referents [#refs]_ + of the given object. + + Certain classes are known to be sub-classes of or to behave as + ``dict`` objects. Function **adict** can be used to register + other class objects to be treated like ``dict``. + +**Public Classes** [#unsafe]_ + + Class **Asizer** may be used to accumulate the results of several + **asizeof** or **asizesof** calls. After creating an **Asizer** + instance, use methods **asizeof** and **asizesof** as needed to + size any number of additional objects. + + Call methods **exclude_refs** and/or **exclude_types** to exclude + references to respectively instances or types of certain objects. + + Use one of the **print\\_...** methods to report the statistics. + + An instance of class **Asized** is returned for each object sized + by the **asized** function or method. + +**Duplicate Objects** + + Any duplicate, given objects are sized only once and the size + is included in the accumulated total only once. But functions + **asizesof** and **asized** will return a size value respectively + an **Asized** instance for each given object, including duplicates. + +**Definitions** [#arb]_ + + The *length* of an objects like ``dict``, ``list``, ``set``, + ``str``, ``tuple``, etc. is defined as the number of items held + in or allocated by the object. Held items are *references* to + other objects, called the *referents*. + + The *size* of an object is defined as the sum of the *flat size* + of the object plus the sizes of any referents [#refs]_. Referents + are visited recursively up to the specified detail level. However, + the size of objects referenced multiple times is included only once + in the total *size*. + + The *flat size* of an object is defined as the *basic size* of the + object plus the *item size* times the number of allocated *items*, + *references* to referents. The *flat size* does include the size + for the *references* to the referents, but not the size of the + referents themselves. + + The *flat size* returned by function *flatsize* equals the result + of function *asizeof* with options *code=True*, *ignored=False*, + *limit=0* and option *align* set to the same value. + + The accurate *flat size* for an object is obtained from function + ``sys.getsizeof()`` where available. Otherwise, the *length* and + *size* of sequence objects as ``dicts``, ``lists``, ``sets``, etc. + is based on an estimate for the number of allocated items. As a + result, the reported *length* and *size* may differ substantially + from the actual *length* and *size*. + + The *basic* and *item size* are obtained from the ``__basicsize__`` + respectively ``__itemsize__`` attributes of the (type of the) + object. Where necessary (e.g. sequence objects), a zero + ``__itemsize__`` is replaced by the size of a corresponding C type. + + The overhead for Python's garbage collector (GC) is included in + the *basic size* of (GC managed) objects as well as the space + needed for ``refcounts`` (used only in certain Python builds). + + Optionally, size values can be aligned to any power-of-2 multiple. + +**Size of (byte)code** + + The *(byte)code size* of objects like classes, functions, methods, + modules, etc. can be included by setting option *code=True*. + + Iterators are handled like sequences: iterated object(s) are sized + like *referents* [#refs]_, but only up to the specified level or + recursion *limit* (and only if function ``gc.get_referents()`` + returns the referent object of iterators). + + Generators are sized as *(byte)code* only, but the generated + objects are never sized. + +**Old- and New-style Classes** + + All old- and new-style ``class``, instance and ``type`` objects are + handled uniformly such that (a) instance objects are distinguished + from class objects and (b) instances of different old-style classes + can be dealt with separately. + + Class and type objects are represented as ``<class ....* def>`` + respectively ``<type ... def>`` where the ``*`` indicates an old-style + class and the ``... def`` suffix marks the *definition object*. + Instances of classes are shown as ``<class module.name*>`` without + the ``... def`` suffix. The ``*`` after the name indicates an + instance of an old-style class. + +**Ignored Objects** + + To avoid excessive sizes, several object types are ignored [#arb]_ + by default, e.g. built-in functions, built-in types and classes + [#bi]_, function globals and module referents. However, any + instances thereof and module objects will be sized when passed as + given objects. Ignored object types are included unless option + *ignored* is set accordingly. + + In addition, many ``__...__`` attributes of callable objects are + ignored [#arb]_, except crucial ones, e.g. class attributes ``__dict__``, + ``__doc__``, ``__name__`` and ``__slots__``. For more details, see + the type-specific ``_..._refs()`` and ``_len_...()`` functions below. + +.. rubric:: Footnotes +.. [#unsafe] The functions and classes in this module are not thread-safe. + +.. [#refs] The *referents* of an object are the objects referenced *by* + that object. For example, the *referents* of a ``list`` are the + objects held in the ``list``, the *referents* of a ``dict`` are + the key and value objects in the ``dict``, etc. + +.. [#arb] These definitions and other assumptions are rather arbitrary + and may need corrections or adjustments. + +.. [#digit] See Python source file ``.../Include/longinterp.h`` for the + C ``typedef`` of ``digit`` used in multi-precision int (or long) + objects. The C ``sizeof(digit)`` in bytes can be obtained in + Python from the int (or long) ``__itemsize__`` attribute. + Function **leng** determines the number of ``digits`` of an int + (or long) object. + +.. [#bi] ``Type``s and ``class``es are considered built-in if the + ``__module__`` of the type or class is listed in the private + ``_builtin_modules``. +''' # PYCHOK escape +import sys +if sys.version_info < (3, 6, 0): + raise NotImplementedError('%s requires Python 3.6 or newer' % ('asizeof',)) + +from typing import Callable, Dict, List, Optional, Tuple, Union + +# all imports listed explicitly to help PyChecker +from inspect import (isbuiltin, isclass, iscode, isframe, isfunction, + ismethod, ismodule, stack) +from math import log +from os import curdir, linesep +from struct import calcsize # type/class Struct only in Python 2.5+ +import types as Types +import warnings +import weakref as Weakref + +__all__ = ['adict', 'asized', 'asizeof', 'asizesof', + 'Asized', 'Asizer', # classes + 'basicsize', 'flatsize', 'itemsize', 'leng', 'refs'] +__version__ = '21.08.09' + +# Any classes or types in modules listed in _builtin_modules are +# considered built-in and ignored by default, as built-in functions +_builtin_mods = [int.__module__, 'types', Exception.__module__] # 'weakref' +if __name__ != '__main__': # treat this very module as built-in + _builtin_mods.append(__name__) +_builtin_modules = tuple(_builtin_mods) + +# Sizes of some primitive C types +# XXX len(pack(T, 0)) == Struct(T).size == calcsize(T) +_sizeof_Cbyte = calcsize('c') # sizeof(unsigned char) +_sizeof_Clong = calcsize('l') # sizeof(long) +_sizeof_Cvoidp = calcsize('P') # sizeof(void*) + +# sizeof(long) != sizeof(ssize_t) on LLP64 +if _sizeof_Clong < _sizeof_Cvoidp: # pragma: no coverage + _z_P_L = 'P' +else: + _z_P_L = 'L' + + +def _calcsize(fmt): + '''Like struct.calcsize() but handling 'z' for Py_ssize_t. + ''' + return calcsize(fmt.replace('z', _z_P_L)) + + +# Defaults for some basic sizes with 'z' for C Py_ssize_t +_sizeof_CPyCodeObject = _calcsize('Pz10P5i0P') # sizeof(PyCodeObject) +_sizeof_CPyFrameObject = _calcsize('Pzz13P63i0P') # sizeof(PyFrameObject) +_sizeof_CPyModuleObject = _calcsize('PzP0P') # sizeof(PyModuleObject) + +# Defaults for some item sizes with 'z' for C Py_ssize_t +_sizeof_CPyDictEntry = _calcsize('z2P') # sizeof(PyDictEntry) +_sizeof_Csetentry = _calcsize('lP') # sizeof(setentry) + +_sizeof_Cdigit = int.__itemsize__ +if _sizeof_Cdigit < 2: # pragma: no coverage + raise AssertionError('sizeof(%s) bad: %d' % ('digit', _sizeof_Cdigit)) + +_builtins2 = (range,) + +# Get character size for internal unicode representation in Python < 3.3 +u = '\0'.encode('utf-8') +_sizeof_Cunicode = len(u) +del u + +try: # Size of GC header, sizeof(PyGC_Head) + import _testcapi as t + _sizeof_CPyGC_Head = t.SIZEOF_PYGC_HEAD # new in Python 2.6 +except (ImportError, AttributeError): # sizeof(PyGC_Head) + # alignment should be to sizeof(long double) but there + # is no way to obtain that value, assume twice double + t = calcsize('2d') - 1 + _sizeof_CPyGC_Head = (_calcsize('2Pz') + t) & ~t +del t + +# Size of refcounts (Python debug build only) +if hasattr(sys, 'gettotalrefcount'): # pragma: no coverage + _sizeof_Crefcounts = _calcsize('2z') +else: + _sizeof_Crefcounts = 0 + +from abc import ABCMeta + +# Some flags from .../Include/object.h +_Py_TPFLAGS_HEAPTYPE = 1 << 9 # Py_TPFLAGS_HEAPTYPE +_Py_TPFLAGS_HAVE_GC = 1 << 14 # Py_TPFLAGS_HAVE_GC + +_Type_type = type(type) # == type and new-style class type + + +# Compatibility functions for more uniform +# behavior across Python version 2.2 thu 3+ + +def _items(obj): # dict only + '''Return iter-/generator, preferably. + ''' + o = getattr(obj, 'iteritems', obj.items) + if _callable(o): + return o() + else: + return o or () + + +def _keys(obj): # dict only + '''Return iter-/generator, preferably. + ''' + o = getattr(obj, 'iterkeys', obj.keys) + if _callable(o): + return o() + else: + return o or () + + +def _values(obj): # dict only + '''Return iter-/generator, preferably. + ''' + o = getattr(obj, 'itervalues', obj.values) + if _callable(o): + return o() + else: + return o or () + + +try: # callable() builtin + _callable = callable +except NameError: # callable() removed in Python 3+ + def _callable(obj): + '''Substitute for callable().''' + return hasattr(obj, '__call__') + +# 'cell' is holding data used in closures +c = (lambda unused: (lambda: unused))(None) +_cell_type = type(c.__closure__[0]) # type: ignore +del c + +from gc import get_objects as _getobjects # containers only? + +if sys.platform == 'ios': # Apple iOS + _gc_getobjects = _getobjects + + def _getobjects(): # PYCHOK expected + # avoid Pythonista3/Python 3+ crash + return tuple(o for o in _gc_getobjects() if not _isNULL(o)) + +from gc import get_referents as _getreferents + +# sys.getsizeof() new in Python 2.6 +_getsizeof = sys.getsizeof # overridden below +_getsizeof_excls = () # types not sys.getsizeof'd + +from sys import intern as _intern + + +# Private functions + +def _basicsize(t, base=0, heap=False, obj=None): + '''Get non-zero basicsize of type, + including the header sizes. + ''' + s = max(getattr(t, '__basicsize__', 0), base) + # include gc header size + if t != _Type_type: + h = getattr(t, '__flags__', 0) & _Py_TPFLAGS_HAVE_GC + elif heap: # type, allocated on heap + h = True + else: # None has no __flags__ attr + h = getattr(obj, '__flags__', 0) & _Py_TPFLAGS_HEAPTYPE + if h: + s += _sizeof_CPyGC_Head + # include reference counters + return s + _sizeof_Crefcounts + + +def _classof(obj, dflt=None): + '''Return the object's class object. + ''' + return getattr(obj, '__class__', dflt) + + +def _derive_typedef(typ): + '''Return single, existing super type typedef or None. + ''' + v = [v for v in _values(_typedefs) if _issubclass(typ, v.type)] + if len(v) == 1: + return v[0] + return None + + +def _dir2(obj, pref='', excl=(), slots=None, itor=''): + '''Return an attribute name, object 2-tuple for certain + attributes or for the ``__slots__`` attributes of the + given object, but not both. Any iterator referent + objects are returned with the given name if the + latter is non-empty. + ''' + if slots: # __slots__ attrs + if hasattr(obj, slots): + # collect all inherited __slots__ attrs + # from list, tuple, or dict __slots__, + # while removing any duplicate attrs + s = {} + for c in type(obj).mro(): + for a in getattr(c, slots, ()): + if a.startswith('__'): + a = '_' + c.__name__ + a + if hasattr(obj, a): + s.setdefault(a, getattr(obj, a)) + # assume __slots__ tuple-like is holding the values + # yield slots, _Slots(s) # _keys(s) ... REMOVED, + # see _Slots.__doc__ further below + for t in _items(s): + yield t # attr name, value + elif itor: # iterator referents + for o in obj: # iter(obj) + yield itor, o + else: # regular attrs + for a in dir(obj): + if a.startswith(pref) and hasattr(obj, a) and a not in excl: + yield a, getattr(obj, a) + + +def _getsizeof_excls_add(typ): + '''Add another type to the tuple of types to be + excluded from sys.getsizeof due to errors. + ''' + global _getsizeof_excls + if typ and typ not in _getsizeof_excls: + _getsizeof_excls += (typ,) + + +def _infer_dict(obj): + '''Return True for likely dict object via duck typing. + ''' + for attrs in (('items', 'keys', 'values'), # 'update', + ('iteritems', 'iterkeys', 'itervalues')): + attrs += '__len__', 'get', 'has_key' + if all(_callable(getattr(obj, a, None)) for a in attrs): + return True + return False + + +def _isbuiltin2(obj): + '''Return True for builtins like Python 2. + ''' + # range is no longer a built-in in Python 3+ + return isbuiltin(obj) or obj in _builtins2 + + +def _iscell(obj): + '''Return True if obj is a cell as used in a closure. + ''' + return isinstance(obj, _cell_type) + + +def _isdictclass(obj): + '''Return True for known dict objects. + ''' + c = _classof(obj) + return c and c.__name__ in _dict_classes.get(c.__module__, ()) + + +def _isframe(obj): + '''Return True for a stack frame object. + ''' + try: # safe isframe(), see pympler.muppy + return isframe(obj) + except ReferenceError: + return False + + +def _isnamedtuple(obj): + '''Named tuples are identified via duck typing: + <http://www.gossamer-threads.com/lists/python/dev/1142178> + ''' + return isinstance(obj, tuple) and hasattr(obj, '_fields') + + +def _isNULL(obj): + '''Prevent asizeof(all=True, ...) crash. + + Sizing gc.get_objects() crashes in Pythonista3 with + Python 3.5.1 on iOS due to 1-tuple (<Null>,) object, + see <http://forum.omz-software.com/user/mrjean1>. + ''' + return isinstance(obj, tuple) and len(obj) == 1 \ + and repr(obj) == '(<NULL>,)' + + +def _issubclass(sub, sup): + '''Safe issubclass(). + ''' + if sup is not object: + try: + return issubclass(sub, sup) + except TypeError: + pass + return False + + +def _itemsize(t, item=0): + '''Get non-zero itemsize of type. + ''' + # replace zero value with default + return getattr(t, '__itemsize__', 0) or item + + +def _kwdstr(**kwds): + '''Keyword arguments as a string. + ''' + return ', '.join(sorted('%s=%r' % kv for kv in _items(kwds))) + + +def _lengstr(obj): + '''Object length as a string. + ''' + n = leng(obj) + if n is None: # no len + r = '' + elif n > _len(obj): # extended + r = ' leng %d!' % n + else: + r = ' leng %d' % n + return r + + +def _moduleof(obj, dflt=''): + '''Return the object's module name. + ''' + return getattr(obj, '__module__', dflt) + + +def _nameof(obj, dflt=''): + '''Return the name of an object. + ''' + return getattr(obj, '__name__', dflt) + + +def _objs_opts_x(objs, all=None, **opts): + '''Return given or 'all' objects + and the remaining options. + ''' + if objs: # given objects + t = objs + x = False + elif all in (False, None): + t = () + x = True + elif all is True: # 'all' objects + t = _getobjects() + x = True + else: + raise ValueError('invalid option: %s=%r' % ('all', all)) + return t, opts, x + + +def _p100(part, total, prec=1): + '''Return percentage as string. + ''' + r = float(total) + if r: + r = part * 100.0 / r + return '%.*f%%' % (prec, r) + return 'n/a' + + +def _plural(num): + '''Return 's' if plural. + ''' + if num == 1: + s = '' + else: + s = 's' + return s + + +def _power2(n): + '''Find the next power of 2. + ''' + p2 = 16 + while n > p2: + p2 += p2 + return p2 + + +def _prepr(obj, clip=0): + '''Prettify and clip long repr() string. + ''' + return _repr(obj, clip=clip).strip('<>').replace("'", '') # remove <''> + + +def _printf(fmt, *args, **print3options): + '''Formatted print to sys.stdout or given stream. + + *print3options* -- some keyword arguments, like Python 3+ print. + ''' + if print3options: # like Python 3+ + f = print3options.get('file', None) or sys.stdout + if args: + f.write(fmt % args) + else: + f.write(fmt) + f.write(print3options.get('end', linesep)) + if print3options.get('flush', False): + f.flush() + elif args: + print(fmt % args) + else: + print(fmt) + + +def _refs(obj, named, *attrs, **kwds): + '''Return specific attribute objects of an object. + ''' + if named: + _N = _NamedRef + else: + def _N(_, o): + return o + + for a in attrs: # cf. inspect.getmembers() + if hasattr(obj, a): + yield _N(a, getattr(obj, a)) + if kwds: # kwds are _dir2() args + for a, o in _dir2(obj, **kwds): + yield _N(a, o) + + +def _repr(obj, clip=80): + '''Clip long repr() string. + ''' + try: # safe repr() + r = repr(obj).replace(linesep, '\\n') + except Exception: + r = 'N/A' + if len(r) > clip > 0: + h = (clip // 2) - 2 + if h > 0: + r = r[:h] + '....' + r[-h:] + return r + + +def _SI(size, K=1024, i='i'): + '''Return size as SI string. + ''' + if 1 < K <= size: + f = float(size) + for si in iter('KMGPTE'): + f /= K + if f < K: + return ' or %.1f %s%sB' % (f, si, i) + return '' + + +def _SI2(size, **kwds): + '''Return size as regular plus SI string. + ''' + return str(size) + _SI(size, **kwds) + + +# Type-specific referents functions + +def _cell_refs(obj, named): + try: # handle 'empty' cells + o = obj.cell_contents + if named: + o = _NamedRef('cell_contents', o) + yield o + except (AttributeError, ValueError): + pass + + +def _class_refs(obj, named): + '''Return specific referents of a class object. + ''' + return _refs(obj, named, '__class__', '__doc__', '__mro__', + '__name__', '__slots__', '__weakref__', + '__dict__') # __dict__ last + + +def _co_refs(obj, named): + '''Return specific referents of a code object. + ''' + return _refs(obj, named, pref='co_') + + +def _dict_refs(obj, named): + '''Return key and value objects of a dict/proxy. + ''' + try: + if named: + for k, v in _items(obj): + s = str(k) + yield _NamedRef('[K] ' + s, k) + yield _NamedRef('[V] ' + s + ': ' + _repr(v), v) + else: + for k, v in _items(obj): + yield k + yield v + except (KeyError, ReferenceError, TypeError) as x: + warnings.warn("Iterating '%s': %r" % (_classof(obj), x)) + + +def _enum_refs(obj, named): + '''Return specific referents of an enumerate object. + ''' + return _refs(obj, named, '__doc__') + + +def _exc_refs(obj, named): + '''Return specific referents of an Exception object. + ''' + # .message raises DeprecationWarning in Python 2.6 + return _refs(obj, named, 'args', 'filename', 'lineno', 'msg', 'text') # , 'message', 'mixed' + + +def _file_refs(obj, named): + '''Return specific referents of a file object. + ''' + return _refs(obj, named, 'mode', 'name') + + +def _frame_refs(obj, named): + '''Return specific referents of a frame object. + ''' + return _refs(obj, named, pref='f_') + + +def _func_refs(obj, named): + '''Return specific referents of a function or lambda object. + ''' + return _refs(obj, named, '__doc__', '__name__', '__code__', '__closure__', + pref='func_', excl=('func_globals',)) + + +def _gen_refs(obj, named): + '''Return the referent(s) of a generator (expression) object. + ''' + # only some gi_frame attrs + f = getattr(obj, 'gi_frame', None) + return _refs(f, named, 'f_locals', 'f_code') +# do not yield any items to keep generator intact +# for r in _refs(f, named, 'f_locals', 'f_code'): +# yield r +# for r in obj: +# yield r + + +def _im_refs(obj, named): + '''Return specific referents of a method object. + ''' + return _refs(obj, named, '__doc__', '__name__', '__code__', pref='im_') + + +def _inst_refs(obj, named): + '''Return specific referents of a class instance. + ''' + return _refs(obj, named, '__dict__', '__class__', slots='__slots__') + + +def _iter_refs(obj, named): + '''Return the referent(s) of an iterator object. + ''' + r = _getreferents(obj) # special case + return _refs(r, named, itor=_nameof(obj) or 'iteref') + + +def _module_refs(obj, named): + '''Return specific referents of a module object. + ''' + # ignore this very module + if obj.__name__ == __name__: + return () + # module is essentially a dict + return _dict_refs(obj.__dict__, named) + + +def _namedtuple_refs(obj, named): + '''Return specific referents of obj-as-sequence and slots but exclude dict. + ''' + for r in _refs(obj, named, '__class__', slots='__slots__'): + yield r + for r in obj: + yield r + + +def _prop_refs(obj, named): + '''Return specific referents of a property object. + ''' + return _refs(obj, named, '__doc__', pref='f') + + +def _seq_refs(obj, unused): # named unused for PyChecker + '''Return specific referents of a frozen/set, list, tuple and xrange object. + ''' + return obj # XXX for r in obj: yield r + + +def _stat_refs(obj, named): + '''Return referents of a os.stat object. + ''' + return _refs(obj, named, pref='st_') + + +def _statvfs_refs(obj, named): + '''Return referents of a os.statvfs object. + ''' + return _refs(obj, named, pref='f_') + + +def _tb_refs(obj, named): + '''Return specific referents of a traceback object. + ''' + return _refs(obj, named, pref='tb_') + + +def _type_refs(obj, named): + '''Return specific referents of a type object. + ''' + return _refs(obj, named, '__doc__', '__mro__', '__name__', + '__slots__', '__weakref__', '__dict__') + + +def _weak_refs(obj, unused): # named unused for PyChecker + '''Return weakly referent object. + ''' + try: # ignore 'key' of KeyedRef + return (obj(),) + except Exception: # XXX ReferenceError + return () + + +_all_refs = (None, _cell_refs, _class_refs, _co_refs, _dict_refs, _enum_refs, + _exc_refs, _file_refs, _frame_refs, _func_refs, _gen_refs, + _im_refs, _inst_refs, _iter_refs, _module_refs, _namedtuple_refs, + _prop_refs, _seq_refs, _stat_refs, _statvfs_refs, _tb_refs, + _type_refs, _weak_refs) + + +# Type-specific length functions + +def _len(obj): + '''Safe len(). + ''' + try: + return len(obj) + except TypeError: # no len() + return 0 + + +def _len_bytearray(obj): + '''Bytearray size. + ''' + return obj.__alloc__() + + +def _len_code(obj): # see .../Lib/test/test_sys.py + '''Length of code object (stack and variables only). + ''' + return (obj.co_stacksize + obj.co_nlocals + + _len(obj.co_freevars) + _len(obj.co_cellvars) - 1) + + +def _len_dict(obj): + '''Dict length in items (estimate). + ''' + n = len(obj) # active items + if n < 6: # ma_smalltable ... + n = 0 # ... in basicsize + else: # at least one unused + n = _power2(n + 1) + return n + + +def _len_frame(obj): + '''Length of a frame object. + ''' + c = getattr(obj, 'f_code', None) + if c: + n = _len_code(c) + else: + n = 0 + return n + + +_digit2p2 = 1 << (_sizeof_Cdigit << 3) +_digitmax = _digit2p2 - 1 # == (2 * PyLong_MASK + 1) +_digitlog = 1.0 / log(_digit2p2) + + +def _len_int(obj): + '''Length of multi-precision int (aka long) in digits. + ''' + if obj: + n, i = 1, abs(obj) + if i > _digitmax: + # no log(x[, base]) in Python 2.2 + n += int(log(i) * _digitlog) + else: # zero + n = 0 + return n + + +def _len_iter(obj): + '''Length (hint) of an iterator. + ''' + n = getattr(obj, '__length_hint__', None) + if n: + n = n() + else: # try len() + n = _len(obj) + return n + + +def _len_list(obj): + '''Length of list (estimate). + ''' + n = len(obj) + # estimate over-allocation + if n > 8: + n += 6 + (n >> 3) + elif n: + n += 4 + return n + + +def _len_module(obj): + '''Module length. + ''' + return _len(obj.__dict__) # _len(dir(obj)) + + +def _len_set(obj): + '''Length of frozen/set (estimate). + ''' + n = len(obj) + if n > 8: # assume half filled + n = _power2(n + n - 2) + elif n: # at least 8 + n = 8 + return n + + +def _len_slice(obj): + '''Slice length. + ''' + try: + return ((obj.stop - obj.start + 1) // obj.step) + except (AttributeError, TypeError): + return 0 + + +# REMOVED, see _Slots.__doc__ +# def _len_slots(obj): +# '''Slots length. +# ''' +# return len(obj) - 1 + + +def _len_struct(obj): + '''Struct length in bytes. + ''' + try: + return obj.size + except AttributeError: + return 0 + + +def _len_unicode(obj): + '''Unicode size. + ''' + return len(obj) + 1 + + +_all_lens = (None, _len, _len_bytearray, _len_code, _len_dict, + _len_frame, _len_int, _len_iter, _len_list, + _len_module, _len_set, _len_slice, _len_struct, + _len_unicode) # type: Tuple[Union[None, Callable], ...] # _len_array, _len_numpy, _len_slots + + +# More private functions and classes + +_old_style = '*' # marker +_new_style = '' # no marker + + +class _Claskey(object): + '''Wrapper for class objects. + ''' + __slots__ = ('_obj', '_sty') + + def __init__(self, obj, style): + self._obj = obj # XXX Weakref.ref(obj) + self._sty = style + + def __str__(self): + r = str(self._obj) + if r.endswith('>'): + r = '%s%s def>' % (r[:-1], self._sty) + elif self._sty is _old_style and not r.startswith('class '): + r = 'class %s%s def' % (r, self._sty) + else: + r = '%s%s def' % (r, self._sty) + return r + __repr__ = __str__ + + +# For most objects, the object type is used as the key in the +# _typedefs dict further below, except class and type objects +# and old-style instances. Those are wrapped with separate +# _Claskey or _Instkey instances to be able (1) to distinguish +# instances of different old-style classes by class, (2) to +# distinguish class (and type) instances from class (and type) +# definitions for new-style classes and (3) provide similar +# results for repr() and str() of new- and old-style classes +# and instances. + +_claskeys = {} # type: Dict[int, _Claskey] + + +def _claskey(obj, style): + '''Wrap an old- or new-style class object. + ''' + i = id(obj) + k = _claskeys.get(i, None) + if not k: + _claskeys[i] = k = _Claskey(obj, style) + return k + + +def _keytuple(obj): # PYCHOK expected + '''Return class and instance keys for a class. + ''' + if type(obj) is _Type_type: # isclass(obj): + return _claskey(obj, _new_style), obj + return None, None # not a class + + +def _objkey(obj): # PYCHOK expected + '''Return the key for any object. + ''' + k = type(obj) + if k is _Type_type: # isclass(obj): + k = _claskey(obj, _new_style) + return k + + +class _NamedRef(object): + '''Store referred object along + with the name of the referent. + ''' + __slots__ = ('name', 'ref') + + def __init__(self, name, ref): + self.name = name + self.ref = ref + + +# class _Slots(tuple): +# '''Wrapper class for __slots__ attribute at class definition. +# The instance-specific __slots__ attributes are stored in +# a "tuple-like" space inside the instance, see Luciano +# Ramalho, "Fluent Python", page 274+, O'Reilly, 2016 or +# at <http://Books.Google.com/books>, then search for +# "Fluent Python" "Space Savings with the __slots__". +# ''' +# pass + + +# Kinds of _Typedefs +_i = _intern +_all_kinds = (_kind_static, _kind_dynamic, _kind_derived, _kind_ignored, _kind_inferred) = ( + _i('static'), _i('dynamic'), _i('derived'), _i('ignored'), _i('inferred')) +del _i + +_Not_vari = '' # non-variable item size + + +class _Typedef(object): + '''Type definition class. + ''' + __slots__ = { + 'base': 0, # basic size in bytes + 'item': 0, # item size in bytes + 'leng': None, # or _len_...() function + 'refs': None, # or _..._refs() function + 'both': None, # both data and code if True, code only if False + 'kind': None, # _kind_... value + 'type': None, # original type + 'vari': None} # item size attr name or _Not_vari + + def __init__(self, **kwds): + self.reset(**kwds) + + def __lt__(self, unused): # for Python 3+ + return True + + def __repr__(self): + return repr(self.args()) + + def __str__(self): + t = [str(self.base), str(self.item)] + for f in (self.leng, self.refs): + if f: + t.append(f.__name__) + else: + t.append('n/a') + if not self.both: + t.append('(code only)') + return ', '.join(t) + + def args(self): # as args tuple + '''Return all attributes as arguments tuple. + ''' + return (self.base, self.item, self.leng, self.refs, + self.both, self.kind, self.type) + + def dup(self, other=None, **kwds): + '''Duplicate attributes of dict or other typedef. + ''' + if other is None: + d = _dict_typedef.kwds() + else: + d = other.kwds() + d.update(kwds) + self.reset(**d) + + def flat(self, obj, mask=0): + '''Return the aligned flat size. + ''' + s = self.base + if self.leng and self.item > 0: # include items + s += self.leng(obj) * self.item + # workaround sys.getsizeof (and numpy?) bug ... some + # types are incorrectly sized in some Python versions + # (note, isinstance(obj, ()) == False) + if not isinstance(obj, _getsizeof_excls): + s = _getsizeof(obj, s) + if mask: # align + s = (s + mask) & ~mask + return s + + def format(self): + '''Return format dict. + ''' + i = self.item + if self.vari: + i = 'var' + c = n = '' + if not self.both: + c = ' (code only)' + if self.leng: + n = ' (%s)' % _nameof(self.leng) + return dict(base=self.base, item=i, leng=n, code=c, + kind=self.kind) + + def kwds(self): + '''Return all attributes as keywords dict. + ''' + return dict(base=self.base, both=self.both, + item=self.item, kind=self.kind, + leng=self.leng, refs=self.refs, + type=self.type, vari=self.vari) + + def save(self, t, base=0, heap=False): + '''Save this typedef plus its class typedef. + ''' + c, k = _keytuple(t) + if k and k not in _typedefs: # instance key + _typedefs[k] = self + if c and c not in _typedefs: # class key + if t.__module__ in _builtin_modules: + k = _kind_ignored # default + else: + k = self.kind + _typedefs[c] = _Typedef(base=_basicsize(type(t), base=base, heap=heap), + refs=_type_refs, + both=False, kind=k, type=t) + elif t not in _typedefs: + if not _isbuiltin2(t): # array, range, xrange in Python 2.x + s = ' '.join((self.vari, _moduleof(t), _nameof(t))) + s = '%r %s %s' % ((c, k), self.both, s.strip()) + raise KeyError('asizeof typedef %r bad: %s' % (self, s)) + + _typedefs[t] = _Typedef(base=_basicsize(t, base=base), + both=False, kind=_kind_ignored, type=t) + + def set(self, safe_len=False, **kwds): + '''Set one or more attributes. + ''' + if kwds: # double check + d = self.kwds() + d.update(kwds) + self.reset(**d) + if safe_len and self.item: + self.leng = _len + + def reset(self, base=0, item=0, leng=None, refs=None, + both=True, kind=None, type=None, vari=_Not_vari): + '''Reset all specified attributes. + ''' + if base < 0: + raise ValueError('invalid option: %s=%r' % ('base', base)) + else: + self.base = base + if item < 0: + raise ValueError('invalid option: %s=%r' % ('item', item)) + else: + self.item = item + if leng in _all_lens: # XXX or _callable(leng) + self.leng = leng + else: + raise ValueError('invalid option: %s=%r' % ('leng', leng)) + if refs in _all_refs: # XXX or _callable(refs) + self.refs = refs + else: + raise ValueError('invalid option: %s=%r' % ('refs', refs)) + if both in (False, True): + self.both = both + else: + raise ValueError('invalid option: %s=%r' % ('both', both)) + if kind in _all_kinds: + self.kind = kind + else: + raise ValueError('invalid option: %s=%r' % ('kind', kind)) + self.type = type + self.vari = vari or _Not_vari + if str(self.vari) != self.vari: + raise ValueError('invalid option: %s=%r' % ('vari', vari)) + + +_typedefs = {} # type: Dict[type, _Typedef] + + +def _typedef_both(t, base=0, item=0, leng=None, refs=None, + kind=_kind_static, heap=False, vari=_Not_vari): + '''Add new typedef for both data and code. + ''' + v = _Typedef(base=_basicsize(t, base=base), item=_itemsize(t, item), + refs=refs, leng=leng, + both=True, kind=kind, type=t, vari=vari) + v.save(t, base=base, heap=heap) + return v # for _dict_typedef + + +def _typedef_code(t, base=0, refs=None, kind=_kind_static, heap=False): + '''Add new typedef for code only. + ''' + v = _Typedef(base=_basicsize(t, base=base), + refs=refs, + both=False, kind=kind, type=t) + v.save(t, base=base, heap=heap) + return v # for _dict_typedef + + +# Static typedefs for data and code types +_typedef_both(complex) +_typedef_both(float) +_typedef_both(list, refs=_seq_refs, leng=_len_list, item=_sizeof_Cvoidp) # sizeof(PyObject*) +_typedef_both(tuple, refs=_seq_refs, leng=_len, item=_sizeof_Cvoidp) # sizeof(PyObject*) +_typedef_both(property, refs=_prop_refs) +_typedef_both(type(Ellipsis)) +_typedef_both(type(None)) + +# _Slots are "tuple-like", REMOVED see _Slots.__doc__ +# _typedef_both(_Slots, item=_sizeof_Cvoidp, +# leng=_len_slots, # length less one +# refs=None, # but no referents +# heap=True) # plus head + +# dict, dictproxy, dict_proxy and other dict-like types +_dict_typedef = _typedef_both(dict, item=_sizeof_CPyDictEntry, leng=_len_dict, refs=_dict_refs) +# XXX any class __dict__ is <type dict_proxy> in Python 3+? +_typedef_both(type(_Typedef.__dict__), item=_sizeof_CPyDictEntry, leng=_len_dict, refs=_dict_refs) +# other dict-like classes and types may be derived or inferred, +# provided the module and class name is listed here (see functions +# adict, _isdictclass and _infer_dict for further details) +_dict_classes = {'UserDict': ('IterableUserDict', 'UserDict'), + 'weakref': ('WeakKeyDictionary', 'WeakValueDictionary')} +try: # <type module> is essentially a dict + _typedef_both(Types.ModuleType, base=_dict_typedef.base, + item=_dict_typedef.item + _sizeof_CPyModuleObject, + leng=_len_module, refs=_module_refs) +except AttributeError: # missing + pass + +# Newer or obsolete types +from array import array # array type + +def _array_kwds(obj): + if hasattr(obj, 'itemsize'): + v = 'itemsize' + else: + v = _Not_vari + # since item size varies by the array data type, set + # itemsize to 1 byte and use _len_array in bytes; note, + # function itemsize returns the actual size in bytes + # and function leng returns the length in number of items + return dict(leng=_len_array, item=_sizeof_Cbyte, vari=v) + +def _len_array(obj): + '''Array length (in bytes!). + ''' + return len(obj) * obj.itemsize + +_all_lens += (_len_array,) # type: ignore + +_typedef_both(array, **_array_kwds(array('d', []))) + +v = sys.version_info +_array_excl = (v[0] == 2 and v < (2, 7, 4)) or \ + (v[0] == 3 and v < (3, 2, 4)) +if _array_excl: # see function _typedef below + _getsizeof_excls_add(array) + +del v + +try: # bool has non-zero __itemsize__ in 3.0 + _typedef_both(bool) +except NameError: # missing + pass + +try: + _typedef_both(bytearray, item=_sizeof_Cbyte, leng=_len_bytearray) +except NameError: # bytearray new in 2.6, 3.0 + pass +try: + if type(bytes) is not type(str): # bytes is str in 2.6, bytes new in 2.6, 3.0 + _typedef_both(bytes, item=_sizeof_Cbyte, leng=_len) # bytes new in 2.6, 3.0 +except NameError: # missing + pass +# try: # XXX like bytes +# _typedef_both(str8, item=_sizeof_Cbyte, leng=_len) # str8 new in 2.6, 3.0 +# except NameError: # missing +# pass + +try: + _typedef_both(enumerate, refs=_enum_refs) +except NameError: # missing + pass + +try: # Exception is type in Python 3+ + _typedef_both(Exception, refs=_exc_refs) +except Exception: # missing + pass + +try: + _typedef_both(frozenset, item=_sizeof_Csetentry, leng=_len_set, refs=_seq_refs) +except NameError: # missing + pass +try: + _typedef_both(set, item=_sizeof_Csetentry, leng=_len_set, refs=_seq_refs) +except NameError: # missing + pass + +try: # not callable() + _typedef_both(Types.GetSetDescriptorType) +except AttributeError: # missing + pass + +_typedef_both(int, item=_sizeof_Cdigit, leng=_len_int) + +try: # not callable() + _typedef_both(Types.MemberDescriptorType) +except AttributeError: # missing + pass + +try: + _typedef_both(type(NotImplemented)) # == Types.NotImplementedType +except NameError: # missing + pass + +try: # MCCABE 14 + import numpy # NumPy array, matrix, etc. + + def _isnumpy(obj): + '''Return True for a NumPy arange, array, matrix, etc. instance. + ''' + try: + return isinstance(obj, _numpy_types) or (hasattr(obj, 'nbytes') and + _moduleof(_classof(obj)).startswith('numpy')) + except (AttributeError, OSError, ValueError): # on iOS/Pythonista + return False + + def _len_numpy(obj): + '''NumPy array, matrix, etc. length (in bytes!). + ''' + return obj.nbytes # == obj.size * obj.itemsize + + def _numpy_kwds(obj): + b = _getsizeof(obj, 96) - obj.nbytes # XXX 96..144 typical? + # since item size depends on the numpy data type, set + # itemsize to 1 byte and use _len_numpy in bytes; note, + # function itemsize returns the actual size in bytes, + # function alen returns the length in number of items + return dict(base=b, item=_sizeof_Cbyte, # not obj.itemsize + leng=_len_numpy, + refs=_numpy_refs, + vari='itemsize') + + def _numpy_refs(obj, named): + '''Return the .base object for NumPy slices, views, etc. + ''' + return _refs(obj, named, 'base') + + _all_lens += (_len_numpy,) + _all_refs += (_numpy_refs,) + + v = tuple(map(int, numpy.__version__.split('.')[:2])) + + if v < (1, 19): + t = (numpy.matrix(range(0)),) + else: # numpy.matrix deprecated in 1.19.3 + t = () + _numpy_types = () # type: Tuple[type, ...] + for d in (t + (numpy.array(range(0)), numpy.arange(0), + numpy.ma.masked_array([]), numpy.ndarray(0))): + t = type(d) + if t not in _numpy_types: + _numpy_types += (t,) + if _isnumpy(d): # double check + _typedef_both(t, **_numpy_kwds(d)) + else: + raise AssertionError('not %s: %r' % ('numpy', d)) + + # sizing numpy 1.13 arrays works fine, but 1.8 and older + # appears to suffer from sys.getsizeof() bug like array + _numpy_excl = v < (1, 9) + if _numpy_excl: # see function _typedef below + for t in _numpy_types: + _getsizeof_excls_add(t) + + del d, t, v +except ImportError: # no NumPy + _numpy_excl = numpy = None # type: ignore # see function _typedef below + + def _isnumpy(obj): # PYCHOK expected + '''Not applicable, no NumPy. + ''' + return False + +try: + _typedef_both(range) +except NameError: # missing + pass + +try: + _typedef_both(reversed, refs=_enum_refs) +except NameError: # missing + pass + +try: + _typedef_both(slice, item=_sizeof_Cvoidp, leng=_len_slice) # XXX worst-case itemsize? +except NameError: # missing + pass + +try: + from os import stat + _typedef_both(type(stat(curdir)), refs=_stat_refs) # stat_result +except ImportError: # missing + pass + +try: + from os import statvfs + _typedef_both(type(statvfs(curdir)), refs=_statvfs_refs, # statvfs_result + item=_sizeof_Cvoidp, leng=_len) +except ImportError: # missing + pass + +try: + from struct import Struct # only in Python 2.5 and 3.0 + _typedef_both(Struct, item=_sizeof_Cbyte, leng=_len_struct) # len in bytes +except ImportError: # missing + pass + +try: + _typedef_both(Types.TracebackType, refs=_tb_refs) +except AttributeError: # missing + pass + +_typedef_both(str, leng=_len_unicode, item=_sizeof_Cunicode) + +try: # <type 'KeyedRef'> + _typedef_both(Weakref.KeyedRef, refs=_weak_refs, heap=True) # plus head +except AttributeError: # missing + pass + +try: # <type 'weakproxy'> + _typedef_both(Weakref.ProxyType) +except AttributeError: # missing + pass + +try: # <type 'weakref'> + _typedef_both(Weakref.ReferenceType, refs=_weak_refs) +except AttributeError: # missing + pass + +# some other, callable types +_typedef_code(object, kind=_kind_ignored) +_typedef_code(super, kind=_kind_ignored) +_typedef_code(_Type_type, kind=_kind_ignored) + +try: + _typedef_code(classmethod, refs=_im_refs) +except NameError: + pass +try: + _typedef_code(staticmethod, refs=_im_refs) +except NameError: + pass +try: + _typedef_code(Types.MethodType, refs=_im_refs) +except NameError: + pass + +try: # generator (expression), no itemsize, no len(), not callable() + _typedef_both(Types.GeneratorType, refs=_gen_refs) +except AttributeError: # missing + pass + +try: # <type 'weakcallableproxy'> + _typedef_code(Weakref.CallableProxyType, refs=_weak_refs) +except AttributeError: # missing + pass + +# any type-specific iterators +s = [_items({}), _keys({}), _values({})] +try: # reversed list and tuples iterators + s.extend([reversed([]), reversed(())]) +except NameError: # missing + pass + +try: # callable-iterator + from re import finditer + s.append(finditer('', '')) +except ImportError: # missing + pass + +for t in _values(_typedefs): + if t.type and t.leng: + try: # create an (empty) instance + s.append(t.type()) + except TypeError: + pass +for t in s: + try: + i = iter(t) + _typedef_both(type(i), leng=_len_iter, refs=_iter_refs, item=0) # no itemsize! + except (KeyError, TypeError): # ignore non-iterables, duplicates, etc. + pass +del i, s, t + + +def _typedef(obj, derive=False, frames=False, infer=False): # MCCABE 25 + '''Create a new typedef for an object. + ''' + t = type(obj) + v = _Typedef(base=_basicsize(t, obj=obj), + kind=_kind_dynamic, type=t) +# _printf('new %r %r/%r %s', t, _basicsize(t), _itemsize(t), _repr(dir(obj))) + if ismodule(obj): # handle module like dict + v.dup(item=_dict_typedef.item + _sizeof_CPyModuleObject, + leng=_len_module, + refs=_module_refs) + elif _isframe(obj): + v.set(base=_basicsize(t, base=_sizeof_CPyFrameObject, obj=obj), + item=_itemsize(t), + leng=_len_frame, + refs=_frame_refs) + if not frames: # ignore frames + v.set(kind=_kind_ignored) + elif iscode(obj): + v.set(base=_basicsize(t, base=_sizeof_CPyCodeObject, obj=obj), + item=_sizeof_Cvoidp, + leng=_len_code, + refs=_co_refs, + both=False) # code only + elif _callable(obj): + if isclass(obj): # class or type + v.set(refs=_class_refs, + both=False) # code only + if _moduleof(obj) in _builtin_modules: + v.set(kind=_kind_ignored) + elif isbuiltin(obj): # function or method + v.set(both=False, # code only + kind=_kind_ignored) + elif isfunction(obj): + v.set(refs=_func_refs, + both=False) # code only + elif ismethod(obj): + v.set(refs=_im_refs, + both=False) # code only + elif isclass(t): # callable instance, e.g. SCons, + # handle like any other instance further below + v.set(item=_itemsize(t), safe_len=True, + refs=_inst_refs) # not code only! + else: + v.set(both=False) # code only + elif _issubclass(t, dict): + v.dup(kind=_kind_derived) + elif _isdictclass(obj) or (infer and _infer_dict(obj)): + v.dup(kind=_kind_inferred) + elif _iscell(obj): + v.set(item=_itemsize(t), refs=_cell_refs) + elif _isnamedtuple(obj): + v.set(refs=_namedtuple_refs) + elif numpy and _isnumpy(obj): # NumPy data + v.set(**_numpy_kwds(obj)) + if _numpy_excl: + _getsizeof_excls_add(t) + elif array and isinstance(obj, array): + v.set(**_array_kwds(obj)) + if _array_excl: + _getsizeof_excls_add(t) + elif _moduleof(obj) in _builtin_modules: + v.set(kind=_kind_ignored) + else: # assume an instance of some class + if derive: + p = _derive_typedef(t) + if p: # duplicate parent + v.dup(other=p, kind=_kind_derived) + return v + if _issubclass(t, Exception): + v.set(item=_itemsize(t), safe_len=True, + refs=_exc_refs, + kind=_kind_derived) + elif isinstance(obj, Exception): + v.set(item=_itemsize(t), safe_len=True, + refs=_exc_refs) + else: + v.set(item=_itemsize(t), safe_len=True, + refs=_inst_refs) + return v + + +class _Prof(object): + '''Internal type profile class. + ''' + high = 0 # largest size + number = 0 # number of (unique) objects + objref = None # largest obj (weakref) + total = 0 # total size + weak = False # objref is weakref(obj) + + def __cmp__(self, other): + if self.total < other.total: + return -1 + elif self.total > other.total: + return +1 + elif self.number < other.number: + return -1 + elif self.number > other.number: + return +1 + return 0 + + def __lt__(self, other): # for Python 3+ + return self.__cmp__(other) < 0 + + def format(self, clip=0, grand=None): + '''Return format dict. + ''' + if self.number > 1: # avg., plural + a, p = int(self.total / self.number), 's' + else: + a, p = self.total, '' + o = self.objref + if self.weak: + o = o() + t = _SI2(self.total) + if grand: + t += ' (%s)' % _p100(self.total, grand, prec=0) + return dict(avg=_SI2(a), high=_SI2(self.high), + lengstr=_lengstr(o), obj=_repr(o, clip=clip), + plural=p, total=t) + + def update(self, obj, size): + '''Update this profile. + ''' + self.number += 1 + self.total += size + if self.high < size: # largest + self.high = size + try: # prefer using weak ref + self.objref, self.weak = Weakref.ref(obj), True + except TypeError: + self.objref, self.weak = obj, False + + +class _Rank(object): + '''Internal largest object class. + ''' + __slots__ = { + 'deep': 0, # recursion depth + 'id': 0, # obj id + 'key': None, # Typedef + 'objref': None, # obj or Weakref.ref(obj) + 'pid': 0, # parent obj id + 'size': 0, # size in bytes + 'weak': False} # objref is Weakref.ref + + def __init__(self, key, obj, size, deep, pid): + self.deep = deep + self.id = id(obj) + self.key = key + try: # prefer using weak ref + self.objref, self.weak = Weakref.ref(obj), True + except TypeError: + self.objref, self.weak = obj, False + self.pid = pid + self.size = size + + def format(self, clip=0, id2x={}): + '''Return string. + ''' + o = self.objref + if self.weak: + o = o() + if self.deep > 0: + d = ' (at %s)' % (self.deep,) + else: + d = '' + if self.pid: + p = ', pix %s' % (id2x.get(self.pid, '?'),) + else: + p = '' + return '%s: %s%s, ix %d%s%s' % (_prepr(self.key, clip=clip), + _repr(o, clip=clip), _lengstr(o), id2x[self.id], d, p) + + +class _Seen(dict): + '''Internal obj visits counter. + ''' + def again(self, key): + try: + s = self[key] + 1 + except KeyError: + s = 1 + if s > 0: + self[key] = s + + +# Public classes + +class Asized(object): + '''Stores the results of an **asized** object in the following + 4 attributes: + + *size* -- total size of the object (including referents) + + *flat* -- flat size of the object (in bytes) + + *name* -- name or ``repr`` of the object + + *refs* -- tuple containing an **Asized** instance for each referent + ''' + __slots__ = ('flat', 'name', 'refs', 'size') + + def __init__(self, size, flat, refs=(), name=None): + self.size = size # total size + self.flat = flat # flat size + self.name = name # name, repr or None + self.refs = tuple(refs) + + def __str__(self): + return 'size %r, flat %r, refs[%d], name %r' % ( + self.size, self.flat, len(self.refs), self.name) + + def format(self, format='%(name)s size=%(size)d flat=%(flat)d', + depth=-1, order_by='size', indent=''): + '''Format the size information of the object and of all + sized referents as a string. + + *format* -- Specifies the format per instance (with 'name', + 'size' and 'flat' as interpolation parameters) + + *depth* -- Recursion level up to which the referents are + printed (use -1 for unlimited) + + *order_by* -- Control sort order of referents, valid choices + are 'name', 'size' and 'flat' + + *indent* -- Optional indentation (default '') + ''' + t = indent + (format % dict(size=self.size, flat=self.flat, + name=self.name)) + if depth and self.refs: + rs = sorted(self.refs, key=lambda x: getattr(x, order_by), + reverse=order_by in ('size', 'flat')) + rs = [r.format(format=format, depth=depth-1, order_by=order_by, + indent=indent+' ') for r in rs] + t = '\n'.join([t] + rs) + return t + + def get(self, name, dflt=None): + '''Return the named referent (or *dflt* if not found). + ''' + for ref in self.refs: + if name == ref.name: + return ref + return dflt + + +class Asizer(object): + '''Sizer state and options to accumulate sizes. + ''' + _above_ = 1024 # rank only objs of size 1K+ + _align_ = 8 + _clip_ = 80 + _code_ = False + _cutoff_ = 0 # in percent + _derive_ = False + _detail_ = 0 # for Asized only + _frames_ = False + _infer_ = False + _limit_ = 100 + _stats_ = 0 + + _depth = 0 # deepest recursion + _excl_d = None # {} + _ign_d = _kind_ignored + _incl = '' # or ' (incl. code)' + _mask = 7 # see _align_ + _missed = 0 # due to errors + _profile = False # no profiling + _profs = None # {} + _ranked = 0 + _ranks = [] # type: List[_Rank] # sorted by decreasing size + _seen = None # {} + _stream = None # I/O stream for printing + _total = 0 # total size + + def __init__(self, **opts): + '''New **Asizer** accumulator. + + See this module documentation for more details. + See method **reset** for all available options and defaults. + ''' + self._excl_d = {} + self.reset(**opts) + + def _c100(self, stats): + '''Cutoff as percentage (for backward compatibility) + ''' + s = int(stats) + c = int((stats - s) * 100.0 + 0.5) or self.cutoff + return s, c + + def _clear(self): + '''Clear state. + ''' + self._depth = 0 # recursion depth reached + self._incl = '' # or ' (incl. code)' + self._missed = 0 # due to errors + self._profile = False + self._profs = {} + self._ranked = 0 + self._ranks = [] + self._seen = _Seen() + self._total = 0 # total size + for k in _keys(self._excl_d): + self._excl_d[k] = 0 + # don't size, profile or rank private, possibly large objs + m = sys.modules[__name__] + self.exclude_objs(self, self._excl_d, self._profs, self._ranks, + self._seen, m, m.__dict__, m.__doc__, + _typedefs) + + def _nameof(self, obj): + '''Return the object's name. + ''' + return _nameof(obj, '') or self._repr(obj) + + def _prepr(self, obj): + '''Like **prepr()**. + ''' + return _prepr(obj, clip=self._clip_) + + def _printf(self, fmt, *args, **print3options): + '''Print to sys.stdout or the configured stream if any is + specified and if the file keyword argument is not already + set in the **print3options** for this specific call. + ''' + if self._stream and not print3options.get('file', None): + if args: + fmt = fmt % args + _printf(fmt, file=self._stream, **print3options) + else: + _printf(fmt, *args, **print3options) + + def _prof(self, key): + '''Get _Prof object. + ''' + p = self._profs.get(key, None) + if not p: + self._profs[key] = p = _Prof() + self.exclude_objs(p) # XXX superfluous? + return p + + def _rank(self, key, obj, size, deep, pid): + '''Rank 100 largest objects by size. + ''' + rs = self._ranks + # bisect, see <http://GitHub.com/python/cpython/blob/master/Lib/bisect.py> + i, j = 0, len(rs) + while i < j: + m = (i + j) // 2 + if size < rs[m].size: + i = m + 1 + else: + j = m + if i < 100: + r = _Rank(key, obj, size, deep, pid) + rs.insert(i, r) + self.exclude_objs(r) # XXX superfluous? + while len(rs) > 100: + rs.pop() + # self._ranks[:] = rs[:100] + self._ranked += 1 + + def _repr(self, obj): + '''Like ``repr()``. + ''' + return _repr(obj, clip=self._clip_) + + def _sizer(self, obj, pid, deep, sized): # MCCABE 19 + '''Size an object, recursively. + ''' + s, f, i = 0, 0, id(obj) + if i not in self._seen: + self._seen[i] = 1 + elif deep or self._seen[i]: + # skip obj if seen before + # or if ref of a given obj + self._seen.again(i) + if sized: + s = sized(s, f, name=self._nameof(obj)) + self.exclude_objs(s) + return s # zero + else: # deep == seen[i] == 0 + self._seen.again(i) + try: + k, rs = _objkey(obj), [] + if k in self._excl_d: + self._excl_d[k] += 1 + else: + v = _typedefs.get(k, None) + if not v: # new typedef + _typedefs[k] = v = _typedef(obj, derive=self._derive_, + frames=self._frames_, + infer=self._infer_) + if (v.both or self._code_) and v.kind is not self._ign_d: + s = f = v.flat(obj, self._mask) # flat size + if self._profile: + # profile based on *flat* size + self._prof(k).update(obj, s) + # recurse, but not for nested modules + if v.refs and deep < self._limit_ \ + and not (deep and ismodule(obj)): + # add sizes of referents + z, d = self._sizer, deep + 1 + if sized and deep < self._detail_: + # use named referents + self.exclude_objs(rs) + for o in v.refs(obj, True): + if isinstance(o, _NamedRef): + r = z(o.ref, i, d, sized) + r.name = o.name + else: + r = z(o, i, d, sized) + r.name = self._nameof(o) + rs.append(r) + s += r.size + else: # just size and accumulate + for o in v.refs(obj, False): + s += z(o, i, d, None) + # deepest recursion reached + if self._depth < d: + self._depth = d + if self._stats_ and s > self._above_ > 0: + # rank based on *total* size + self._rank(k, obj, s, deep, pid) + except RuntimeError: # XXX RecursionLimitExceeded: + self._missed += 1 + if not deep: + self._total += s # accumulate + if sized: + s = sized(s, f, name=self._nameof(obj), refs=rs) + self.exclude_objs(s) + return s + + def _sizes(self, objs, sized=None): + '''Return the size or an **Asized** instance for each + given object plus the total size. The total includes + the size of duplicates only once. + ''' + self.exclude_refs(*objs) # skip refs to objs + s, t = {}, [] + self.exclude_objs(s, t) + for o in objs: + i = id(o) + if i in s: # duplicate + self._seen.again(i) + else: + s[i] = self._sizer(o, 0, 0, sized) + t.append(s[i]) + return tuple(t) + + @property + def above(self): + '''Get the large object size threshold (int). + ''' + return self._above_ + + @property + def align(self): + '''Get the size alignment (int). + ''' + return self._align_ + + def asized(self, *objs, **opts): + '''Size each object and return an **Asized** instance with + size information and referents up to the given detail + level (and with modified options, see method **set**). + + If only one object is given, the return value is the + **Asized** instance for that object. The **Asized** size + of duplicate and ignored objects will be zero. + ''' + if opts: + self.set(**opts) + t = self._sizes(objs, Asized) + if len(t) == 1: + t = t[0] + return t + + def asizeof(self, *objs, **opts): + '''Return the combined size of the given objects + (with modified options, see method **set**). + ''' + if opts: + self.set(**opts) + self.exclude_refs(*objs) # skip refs to objs + return sum(self._sizer(o, 0, 0, None) for o in objs) + + def asizesof(self, *objs, **opts): + '''Return the individual sizes of the given objects + (with modified options, see method **set**). + + The size of duplicate and ignored objects will be zero. + ''' + if opts: + self.set(**opts) + return self._sizes(objs, None) + + @property + def clip(self): + '''Get the clipped string length (int). + ''' + return self._clip_ + + @property + def code(self): + '''Size (byte) code (bool). + ''' + return self._code_ + + @property + def cutoff(self): + '''Stats cutoff (int). + ''' + return self._cutoff_ + + @property + def derive(self): + '''Derive types (bool). + ''' + return self._derive_ + + @property + def detail(self): + '''Get the detail level for **Asized** refs (int). + ''' + return self._detail_ + + @property + def duplicate(self): + '''Get the number of duplicate objects seen so far (int). + ''' + return sum(1 for v in _values(self._seen) if v > 1) # == len + + def exclude_objs(self, *objs): + '''Exclude the specified objects from sizing, profiling and ranking. + ''' + for o in objs: + self._seen.setdefault(id(o), -1) + + def exclude_refs(self, *objs): + '''Exclude any references to the specified objects from sizing. + + While any references to the given objects are excluded, the + objects will be sized if specified as positional arguments + in subsequent calls to methods **asizeof** and **asizesof**. + ''' + for o in objs: + self._seen.setdefault(id(o), 0) + + def exclude_types(self, *objs): + '''Exclude the specified object instances and types from sizing. + + All instances and types of the given objects are excluded, + even objects specified as positional arguments in subsequent + calls to methods **asizeof** and **asizesof**. + ''' + for o in objs: + for t in _keytuple(o): + if t and t not in self._excl_d: + self._excl_d[t] = 0 + + @property + def excluded(self): + '''Get the types being excluded (tuple). + ''' + return tuple(_keys(self._excl_d)) + + @property + def frames(self): + '''Ignore stack frames (bool). + ''' + return self._frames_ + + @property + def ignored(self): + '''Ignore certain types (bool). + ''' + return True if self._ign_d else False + + @property + def infer(self): + '''Infer types (bool). + ''' + return self._infer_ + + @property + def limit(self): + '''Get the recursion limit (int). + ''' + return self._limit_ + + @property + def missed(self): + '''Get the number of objects missed due to errors (int). + ''' + return self._missed + + def print_largest(self, w=0, cutoff=0, **print3options): + '''Print the largest objects. + + The available options and defaults are: + + *w=0* -- indentation for each line + + *cutoff=100* -- number of largest objects to print + + *print3options* -- some keyword arguments, like Python 3+ print + ''' + c = int(cutoff) if cutoff else self._cutoff_ + n = min(len(self._ranks), max(c, 0)) + s = self._above_ + if n > 0 and s > 0: + self._printf('%s%*d largest object%s (of %d over %d bytes%s)', linesep, + w, n, _plural(n), self._ranked, s, _SI(s), **print3options) + id2x = dict((r.id, i) for i, r in enumerate(self._ranks)) + for r in self._ranks[:n]: + s, t = r.size, r.format(self._clip_, id2x) + self._printf('%*d bytes%s: %s', w, s, _SI(s), t, **print3options) + + def print_profiles(self, w=0, cutoff=0, **print3options): + '''Print the profiles above *cutoff* percentage. + + The available options and defaults are: + + *w=0* -- indentation for each line + + *cutoff=0* -- minimum percentage printed + + *print3options* -- some keyword arguments, like Python 3+ print + ''' + # get the profiles with non-zero size or count + t = [(v, k) for k, v in _items(self._profs) if v.total > 0 or v.number > 1] + if (len(self._profs) - len(t)) < 9: # just show all + t = [(v, k) for k, v in _items(self._profs)] + if t: + s = '' + if self._total: + s = ' (% of grand total)' + c = int(cutoff) if cutoff else self._cutoff_ + C = int(c * 0.01 * self._total) + else: + C = c = 0 + self._printf('%s%*d profile%s: total%s, average, and largest flat size%s: largest object', + linesep, w, len(t), _plural(len(t)), s, self._incl, **print3options) + r = len(t) + for v, k in sorted(t, reverse=True): + s = 'object%(plural)s: %(total)s, %(avg)s, %(high)s: %(obj)s%(lengstr)s' % v.format(self._clip_, self._total) + self._printf('%*d %s %s', w, v.number, self._prepr(k), s, **print3options) + r -= 1 + if r > 1 and v.total < C: + self._printf('%+*d profiles below cutoff (%.0f%%)', w, r, c) + break + z = len(self._profs) - len(t) + if z > 0: + self._printf('%+*d %r object%s', w, z, 'zero', _plural(z), **print3options) + + def print_stats(self, objs=(), opts={}, sized=(), sizes=(), stats=3, **print3options): + '''Prints the statistics. + + The available options and defaults are: + + *w=0* -- indentation for each line + + *objs=()* -- optional, list of objects + + *opts={}* -- optional, dict of options used + + *sized=()* -- optional, tuple of **Asized** instances returned + + *sizes=()* -- optional, tuple of sizes returned + + *stats=3* -- print stats, see function **asizeof** + + *print3options* -- some keyword arguments, like Python 3+ print + ''' + s = min(opts.get('stats', stats) or 0, self.stats) + if s > 0: # print stats + w = len(str(self.missed + self.seen + self.total)) + 1 + t = c = '' + o = _kwdstr(**opts) + if o and objs: + c = ', ' + # print header line(s) + if sized and objs: + n = len(objs) + if n > 1: + self._printf('%sasized(...%s%s) ...', linesep, c, o, **print3options) + for i in range(n): # no enumerate in Python 2.2.3 + self._printf('%*d: %s', w - 1, i, sized[i], **print3options) + else: + self._printf('%sasized(%s): %s', linesep, o, sized, **print3options) + elif sizes and objs: + self._printf('%sasizesof(...%s%s) ...', linesep, c, o, **print3options) + for z, o in zip(sizes, objs): + self._printf('%*d bytes%s%s: %s', w, z, _SI(z), self._incl, self._repr(o), **print3options) + else: + if objs: + t = self._repr(objs) + self._printf('%sasizeof(%s%s%s) ...', linesep, t, c, o, **print3options) + # print summary + self.print_summary(w=w, objs=objs, **print3options) + # for backward compatibility, cutoff from fractional stats + s, c = self._c100(s) + self.print_largest(w=w, cutoff=c if s < 2 else 10, **print3options) + if s > 1: # print profile + self.print_profiles(w=w, cutoff=c, **print3options) + if s > 2: # print typedefs + self.print_typedefs(w=w, **print3options) # PYCHOK .print_largest? + + def print_summary(self, w=0, objs=(), **print3options): + '''Print the summary statistics. + + The available options and defaults are: + + *w=0* -- indentation for each line + + *objs=()* -- optional, list of objects + + *print3options* -- some keyword arguments, like Python 3+ print + ''' + self._printf('%*d bytes%s%s', w, self._total, _SI(self._total), self._incl, **print3options) + if self._mask: + self._printf('%*d byte aligned', w, self._mask + 1, **print3options) + self._printf('%*d byte sizeof(void*)', w, _sizeof_Cvoidp, **print3options) + n = len(objs or ()) + self._printf('%*d object%s %s', w, n, _plural(n), 'given', **print3options) + n = self.sized + self._printf('%*d object%s %s', w, n, _plural(n), 'sized', **print3options) + if self._excl_d: + n = sum(_values(self._excl_d)) + self._printf('%*d object%s %s', w, n, _plural(n), 'excluded', **print3options) + n = self.seen + self._printf('%*d object%s %s', w, n, _plural(n), 'seen', **print3options) + n = self.ranked + if n > 0: + self._printf('%*d object%s %s', w, n, _plural(n), 'ranked', **print3options) + n = self.missed + self._printf('%*d object%s %s', w, n, _plural(n), 'missed', **print3options) + n = self.duplicate + self._printf('%*d duplicate%s', w, n, _plural(n), **print3options) + if self._depth > 0: + self._printf('%*d deepest recursion', w, self._depth, **print3options) + + def print_typedefs(self, w=0, **print3options): + '''Print the types and dict tables. + + The available options and defaults are: + + *w=0* -- indentation for each line + + *print3options* -- some keyword arguments, like Python 3+ print + ''' + for k in _all_kinds: + # XXX Python 3+ doesn't sort type objects + t = [(self._prepr(a), v) for a, v in _items(_typedefs) + if v.kind == k and (v.both or self._code_)] + if t: + self._printf('%s%*d %s type%s: basicsize, itemsize, _len_(), _refs()', + linesep, w, len(t), k, _plural(len(t)), **print3options) + for a, v in sorted(t): + self._printf('%*s %s: %s', w, '', a, v, **print3options) + # dict and dict-like classes + t = sum(len(v) for v in _values(_dict_classes)) + if t: + self._printf('%s%*d dict/-like classes:', linesep, w, t, **print3options) + for m, v in _items(_dict_classes): + self._printf('%*s %s: %s', w, '', m, self._prepr(v), **print3options) + + @property + def ranked(self): + '''Get the number objects ranked by size so far (int). + ''' + return self._ranked + + def reset(self, above=1024, align=8, clip=80, code=False, # PYCHOK too many args + cutoff=10, derive=False, detail=0, frames=False, ignored=True, + infer=False, limit=100, stats=0, stream=None, **extra): + '''Reset sizing options, state, etc. to defaults. + + The available options and default values are: + + *above=0* -- threshold for largest objects stats + + *align=8* -- size alignment + + *code=False* -- incl. (byte)code size + + *cutoff=10* -- limit large objects or profiles stats + + *derive=False* -- derive from super type + + *detail=0* -- **Asized** refs level + + *frames=False* -- ignore frame objects + + *ignored=True* -- ignore certain types + + *infer=False* -- try to infer types + + *limit=100* -- recursion limit + + *stats=0* -- print statistics, see function **asizeof** + + *stream=None* -- output stream for printing + + See function **asizeof** for a description of the options. + ''' + if extra: + t = _plural(len(extra)), _kwdstr(**extra) + raise KeyError('invalid option%s: %s' % t) + # options + self._above_ = above + self._align_ = align + self._clip_ = clip + self._code_ = code + self._cutoff_ = cutoff + self._derive_ = derive + self._detail_ = detail # for Asized only + self._frames_ = frames + self._infer_ = infer + self._limit_ = limit + self._stats_ = stats + self._stream = stream + if ignored: + self._ign_d = _kind_ignored + else: + self._ign_d = None + # clear state + self._clear() + self.set(align=align, code=code, cutoff=cutoff, stats=stats) + + @property + def seen(self): + '''Get the number objects seen so far (int). + ''' + return sum(v for v in _values(self._seen) if v > 0) + + def set(self, above=None, align=None, code=None, cutoff=None, + frames=None, detail=None, limit=None, stats=None): + '''Set some sizing options. See also **reset**. + + The available options are: + + *above* -- threshold for largest objects stats + + *align* -- size alignment + + *code* -- incl. (byte)code size + + *cutoff* -- limit large objects or profiles stats + + *detail* -- **Asized** refs level + + *frames* -- size or ignore frame objects + + *limit* -- recursion limit + + *stats* -- print statistics, see function **asizeof** + + Any options not set remain unchanged from the previous setting. + ''' + # adjust + if above is not None: + self._above_ = int(above) + if align is not None: + self._align_ = align + if align > 1: + self._mask = align - 1 + if (self._mask & align) != 0: + raise ValueError('invalid option: %s=%r' % ('align', align)) + else: + self._mask = 0 + if code is not None: + self._code_ = code + if code: # incl. (byte)code + self._incl = ' (incl. code)' + if detail is not None: + self._detail_ = detail + if frames is not None: + self._frames_ = frames + if limit is not None: + self._limit_ = limit + if stats is not None: + if stats < 0: + raise ValueError('invalid option: %s=%r' % ('stats', stats)) + # for backward compatibility, cutoff from fractional stats + s, c = self._c100(stats) + self._cutoff_ = int(cutoff) if cutoff else c + self._stats_ = s + self._profile = s > 1 # profile types + + @property + def sized(self): + '''Get the number objects sized so far (int). + ''' + return sum(1 for v in _values(self._seen) if v > 0) + + @property + def stats(self): + '''Get the stats and cutoff setting (float). + ''' + return self._stats_ # + (self._cutoff_ * 0.01) + + @property + def total(self): + '''Get the total size (in bytes) accumulated so far. + ''' + return self._total + + +# Public functions + +def adict(*classes): + '''Install one or more classes to be handled as dict. + ''' + a = True + for c in classes: + # if class is dict-like, add class + # name to _dict_classes[module] + if isclass(c) and _infer_dict(c): + t = _dict_classes.get(c.__module__, ()) + if c.__name__ not in t: # extend tuple + _dict_classes[c.__module__] = t + (c.__name__,) + else: # not a dict-like class + a = False + return a # all installed if True + + +_asizer = Asizer() + + +def asized(*objs, **opts): + '''Return a tuple containing an **Asized** instance for each + object passed as positional argument. + + The available options and defaults are: + + *above=0* -- threshold for largest objects stats + + *align=8* -- size alignment + + *code=False* -- incl. (byte)code size + + *cutoff=10* -- limit large objects or profiles stats + + *derive=False* -- derive from super type + + *detail=0* -- Asized refs level + + *frames=False* -- ignore stack frame objects + + *ignored=True* -- ignore certain types + + *infer=False* -- try to infer types + + *limit=100* -- recursion limit + + *stats=0* -- print statistics + + If only one object is given, the return value is the **Asized** + instance for that object. Otherwise, the length of the returned + tuple matches the number of given objects. + + The **Asized** size of duplicate and ignored objects will be zero. + + Set *detail* to the desired referents level and *limit* to the + maximum recursion depth. + + See function **asizeof** for descriptions of the other options. + ''' + _asizer.reset(**opts) + if objs: + t = _asizer.asized(*objs) + _asizer.print_stats(objs, opts=opts, sized=t) # show opts as _kwdstr + _asizer._clear() + else: + t = () + return t + + +def asizeof(*objs, **opts): + '''Return the combined size (in bytes) of all objects passed + as positional arguments. + + The available options and defaults are: + + *above=0* -- threshold for largest objects stats + + *align=8* -- size alignment + + *clip=80* -- clip ``repr()`` strings + + *code=False* -- incl. (byte)code size + + *cutoff=10* -- limit large objects or profiles stats + + *derive=False* -- derive from super type + + *frames=False* -- ignore stack frame objects + + *ignored=True* -- ignore certain types + + *infer=False* -- try to infer types + + *limit=100* -- recursion limit + + *stats=0* -- print statistics + + Set *align* to a power of 2 to align sizes. Any value less + than 2 avoids size alignment. + + If *all* is True and if no positional arguments are supplied. + size all current gc objects, including module, global and stack + frame objects. + + A positive *clip* value truncates all repr() strings to at + most *clip* characters. + + The (byte)code size of callable objects like functions, + methods, classes, etc. is included only if *code* is True. + + If *derive* is True, new types are handled like an existing + (super) type provided there is one and only of those. + + By default certain base types like object, super, etc. are + ignored. Set *ignored* to False to include those. + + If *infer* is True, new types are inferred from attributes + (only implemented for dict types on callable attributes + as get, has_key, items, keys and values). + + Set *limit* to a positive value to accumulate the sizes of + the referents of each object, recursively up to the limit. + Using *limit=0* returns the sum of the flat sizes of the + given objects. High *limit* values may cause runtime errors + and miss objects for sizing. + + A positive value for *stats* prints up to 9 statistics, (1) + a summary of the number of objects sized and seen and a list + of the largests objects with size over *above* bytes, (2) a + simple profile of the sized objects by type and (3+) up to 6 + tables showing the static, dynamic, derived, ignored, inferred + and dict types used, found respectively installed. + The fractional part of the *stats* value (x 100) is the number + of largest objects shown for (*stats*1.+) or the cutoff + percentage for simple profiles for (*stats*=2.+). For example, + *stats=1.10* shows the summary and the 10 largest objects, + also the default. + + See this module documentation for the definition of flat size. + ''' + t, p, x = _objs_opts_x(objs, **opts) + _asizer.reset(**p) + if t: + if x: # don't size, profile or rank _getobjects tuple + _asizer.exclude_objs(t) + s = _asizer.asizeof(*t) + _asizer.print_stats(objs=t, opts=opts) # show opts as _kwdstr + _asizer._clear() + else: + s = 0 + return s + + +def asizesof(*objs, **opts): + '''Return a tuple containing the size (in bytes) of all objects + passed as positional arguments. + + The available options and defaults are: + + *above=1024* -- threshold for largest objects stats + + *align=8* -- size alignment + + *clip=80* -- clip ``repr()`` strings + + *code=False* -- incl. (byte)code size + + *cutoff=10* -- limit large objects or profiles stats + + *derive=False* -- derive from super type + + *frames=False* -- ignore stack frame objects + + *ignored=True* -- ignore certain types + + *infer=False* -- try to infer types + + *limit=100* -- recursion limit + + *stats=0* -- print statistics + + See function **asizeof** for a description of the options. + + The length of the returned tuple equals the number of given + objects. + + The size of duplicate and ignored objects will be zero. + ''' + _asizer.reset(**opts) + if objs: + t = _asizer.asizesof(*objs) + _asizer.print_stats(objs, opts=opts, sizes=t) # show opts as _kwdstr + _asizer._clear() + else: + t = () + return t + + +def _typedefof(obj, save=False, **opts): + '''Get the typedef for an object. + ''' + k = _objkey(obj) + v = _typedefs.get(k, None) + if not v: # new typedef + v = _typedef(obj, **opts) + if save: + _typedefs[k] = v + return v + + +def basicsize(obj, **opts): + '''Return the basic size of an object (in bytes). + + The available options and defaults are: + + *derive=False* -- derive type from super type + + *infer=False* -- try to infer types + + *save=False* -- save the object's type definition if new + + See this module documentation for the definition of *basic size*. + ''' + b = t = _typedefof(obj, **opts) + if t: + b = t.base + return b + + +def flatsize(obj, align=0, **opts): + '''Return the flat size of an object (in bytes), optionally aligned + to the given power of 2. + + See function **basicsize** for a description of other available options. + + See this module documentation for the definition of *flat size*. + ''' + f = t = _typedefof(obj, **opts) + if t: + if align > 1: + m = align - 1 + if (align & m) != 0: + raise ValueError('invalid option: %s=%r' % ('align', align)) + else: + m = 0 + f = t.flat(obj, mask=m) + return f + + +def itemsize(obj, **opts): + '''Return the item size of an object (in bytes). + + See function **basicsize** for a description of the available options. + + See this module documentation for the definition of *item size*. + ''' + i = t = _typedefof(obj, **opts) + if t: + i, v = t.item, t.vari + if v and i == _sizeof_Cbyte: + i = getattr(obj, v, i) + return i + + +def leng(obj, **opts): + '''Return the length of an object (in items). + + See function **basicsize** for a description of the available options. + ''' + n = t = _typedefof(obj, **opts) + if t: + n = t.leng + if n and _callable(n): + i, v, n = t.item, t.vari, n(obj) + if v and i == _sizeof_Cbyte: + i = getattr(obj, v, i) + if i > _sizeof_Cbyte: + n = n // i + return n + + +def named_refs(obj, **opts): + '''Return all named **referents** of an object (re-using + functionality from **asizeof**). + + Does not return un-named *referents*, e.g. objects in a list. + + See function **basicsize** for a description of the available options. + ''' + rs = [] + v = _typedefof(obj, **opts) + if v: + v = v.refs + if v and _callable(v): + for r in v(obj, True): + try: + rs.append((r.name, r.ref)) + except AttributeError: + pass + return rs + + +def refs(obj, **opts): + '''Return (a generator for) specific *referents* of an object. + + See function **basicsize** for a description of the available options. + ''' + v = _typedefof(obj, **opts) + if v: + v = v.refs + if v and _callable(v): + v = v(obj, False) + return v + + +if __name__ == '__main__': + + if '-v' in sys.argv: + import platform + print('%s %s (Python %s %s)' % (__file__, __version__, + sys.version.split()[0], + platform.architecture()[0])) + + elif '-types' in sys.argv: # print static _typedefs + n = len(_typedefs) + w = len(str(n)) * ' ' + _printf('%s%d type definitions: %s and %s, kind ... %s', linesep, + n, 'basic-', 'itemsize (leng)', '-type[def]s') + for k, td in sorted((_prepr(k), td) for k, td in _items(_typedefs)): + desc = '%(base)s and %(item)s%(leng)s, %(kind)s%(code)s' % td.format() + _printf('%s %s: %s', w, k, desc) + + else: + import gc + collect = False + if '-gc' in sys.argv: + collect = True + gc.collect() + + frames = '-frames' in sys.argv + + # just an example + asizeof(all=True, frames=frames, stats=1, above=1024) # print summary + 10 largest + + if collect: + print('gc.collect() %d' % (gc.collect(),)) + +# License from the initial version of this source file follows: + +# -------------------------------------------------------------------- +# Copyright (c) 2002-2019 -- ProphICy Semiconductor, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# - Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# - Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# - Neither the name of ProphICy Semiconductor, Inc. nor the names +# of its contributors may be used to endorse or promote products +# derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. +# -------------------------------------------------------------------- diff --git a/venv/lib/python3.9/site-packages/pympler/charts.py b/venv/lib/python3.9/site-packages/pympler/charts.py new file mode 100644 index 00000000..050cc027 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/charts.py @@ -0,0 +1,62 @@ +""" +Generate charts from gathered data. + +Requires **matplotlib**. +""" + +from pympler.classtracker_stats import Stats + +try: + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + + def tracker_timespace(filename: str, stats: Stats) -> None: + """ + Create a time-space chart from a ``Stats`` instance. + """ + classlist = list(stats.index.keys()) + classlist.sort() + + for snapshot in stats.snapshots: + stats.annotate_snapshot(snapshot) + + timestamps = [fp.timestamp for fp in stats.snapshots] + offsets = [0] * len(stats.snapshots) + poly_labels = [] + polys = [] + for clsname in classlist: + pct = [fp.classes[clsname]['pct'] for fp in stats.snapshots + if fp.classes and clsname in fp.classes] + if max(pct) > 3.0: + sizes = [fp.classes[clsname]['sum'] for fp in stats.snapshots + if fp.classes and clsname in fp.classes] + sizes = [float(x) / (1024 * 1024) for x in sizes] + sizes = [offset + size for offset, size in zip(offsets, sizes)] + poly = matplotlib.mlab.poly_between(timestamps, offsets, sizes) + polys.append((poly, {'label': clsname})) + poly_labels.append(clsname) + offsets = sizes + + fig = plt.figure(figsize=(10, 4)) + axis = fig.add_subplot(111) + + axis.set_title("Snapshot Memory") + axis.set_xlabel("Execution Time [s]") + axis.set_ylabel("Virtual Memory [MiB]") + + totals = [float(x.asizeof_total) / (1024 * 1024) + for x in stats.snapshots] + axis.plot(timestamps, totals, 'r--', label='Total') + tracked = [float(x.tracked_total) / (1024 * 1024) + for x in stats.snapshots] + axis.plot(timestamps, tracked, 'b--', label='Tracked total') + + for (args, kwds) in polys: + axis.fill(*args, **kwds) + axis.legend(loc=2) # TODO fill legend + fig.savefig(filename) + +except ImportError: + def tracker_timespace(filename: str, stats: Stats) -> None: + pass diff --git a/venv/lib/python3.9/site-packages/pympler/classtracker.py b/venv/lib/python3.9/site-packages/pympler/classtracker.py new file mode 100644 index 00000000..b187e4b3 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/classtracker.py @@ -0,0 +1,590 @@ +""" +The `ClassTracker` is a facility delivering insight into the memory +distribution of a Python program. It can introspect memory consumption of +certain classes and objects. Facilities are provided to track and size +individual objects or all instances of certain classes. Tracked objects are +sized recursively to provide an overview of memory distribution between the +different tracked objects. +""" + +from typing import Any, Callable, Dict, IO, List, Optional, Tuple + +from collections import defaultdict +from functools import partial +from inspect import stack, isclass +from threading import Thread, Lock +from time import sleep, time +from weakref import ref as weakref_ref + +from pympler.classtracker_stats import ConsoleStats +from pympler.util.stringutils import safe_repr + +import pympler.asizeof as asizeof +import pympler.process + + +__all__ = ["ClassTracker"] + +# Fixpoint for program start relative time stamp. +_local_start = time() + + +class _ClassObserver(object): + """ + Stores options for tracked classes. + The observer also keeps the original constructor of the observed class. + """ + __slots__ = ('init', 'name', 'detail', 'keep', 'trace') + + def __init__(self, init: Callable, name: str, detail: int, keep: bool, + trace: bool): + self.init = init + self.name = name + self.detail = detail + self.keep = keep + self.trace = trace + + def modify(self, name: str, detail: int, keep: bool, trace: bool) -> None: + self.name = name + self.detail = detail + self.keep = keep + self.trace = trace + + +def _get_time() -> float: + """ + Get a timestamp relative to the program start time. + """ + return time() - _local_start + + +class TrackedObject(object): + """ + Stores size and lifetime information of a tracked object. A weak reference + is attached to monitor the object without preventing its deletion. + """ + __slots__ = ("ref", "id", "repr", "name", "birth", "death", "trace", + "snapshots", "_resolution_level", "__dict__") + + def __init__(self, instance: Any, name: str, resolution_level: int = 0, + trace: bool = False, on_delete: Optional[Callable] = None): + """ + Create a weak reference for 'instance' to observe an object but which + won't prevent its deletion (which is monitored by the finalize + callback). The size of the object is recorded in 'snapshots' as + (timestamp, size) tuples. + """ + self.ref = weakref_ref(instance, self.finalize) + self.id = id(instance) + self.repr = '' + self.name = name + self.birth = _get_time() + self.death = None # type: Optional[float] + self._resolution_level = resolution_level + self.trace = None # type: Optional[List[Tuple]] + + if trace: + self._save_trace() + + initial_size = asizeof.basicsize(instance) or 0 + size = asizeof.Asized(initial_size, initial_size) + self.snapshots = [(self.birth, size)] + self.on_delete = on_delete + + def __getstate__(self) -> Dict: + """ + Make the object serializable for dump_stats. Read the available slots + and store the values in a dictionary. Derived values (stored in the + dict) are not pickled as those can be reconstructed based on the other + data. References cannot be serialized, ignore 'ref' as well. + """ + state = {} + for name in getattr(TrackedObject, '__slots__', ()): + if hasattr(self, name) and name not in ['ref', '__dict__']: + state[name] = getattr(self, name) + return state + + def __setstate__(self, state: Dict) -> None: + """ + Restore the state from pickled data. Needed because a slotted class is + used. + """ + for key, value in list(state.items()): + setattr(self, key, value) + + def _save_trace(self) -> None: + """ + Save current stack trace as formatted string. + """ + stack_trace = stack() + try: + self.trace = [] + for frm in stack_trace[5:]: # eliminate our own overhead + self.trace.insert(0, frm[1:]) + finally: + del stack_trace + + def track_size(self, ts: float, sizer: asizeof.Asizer) -> None: + """ + Store timestamp and current size for later evaluation. + The 'sizer' is a stateful sizing facility that excludes other tracked + objects. + """ + obj = self.ref() + self.snapshots.append( + (ts, sizer.asized(obj, detail=self._resolution_level)) + ) + if obj is not None: + self.repr = safe_repr(obj, clip=128) + + def get_max_size(self) -> int: + """ + Get the maximum of all sampled sizes. + """ + return max([s.size for (_, s) in self.snapshots]) + + def get_size_at_time(self, timestamp: float) -> int: + """ + Get the size of the object at a specific time (snapshot). + If the object was not alive/sized at that instant, return 0. + """ + size = 0 + for (t, s) in self.snapshots: + if t == timestamp: + size = s.size + return size + + def set_resolution_level(self, resolution_level: int) -> None: + """ + Set resolution level to a new value. The next size estimation will + respect the new value. This is useful to set different levels for + different instances of tracked classes. + """ + self._resolution_level = resolution_level + + def finalize(self, ref: weakref_ref) -> None: + """ + Mark the reference as dead and remember the timestamp. It would be + great if we could measure the pre-destruction size. Unfortunately, the + object is gone by the time the weakref callback is called. However, + weakref callbacks are useful to be informed when tracked objects died + without the need of destructors. + + If the object is destroyed at the end of the program execution, it's + not possible to import modules anymore. Hence, the finalize callback + just does nothing (self.death stays None). + """ + try: + self.death = _get_time() + if self.on_delete: + self.on_delete() + except Exception: # pragma: no cover + pass + + +def track_object_creation(time_series: List[Tuple[float, int]]) -> None: + num_instances = time_series[-1][1] if time_series else 0 + time_series.append((_get_time(), num_instances+1)) + + +def track_object_deletion(time_series: List[Tuple[float, int]]) -> None: + num_instances = time_series[-1][1] + time_series.append((_get_time(), num_instances-1)) + + +class PeriodicThread(Thread): + """ + Thread object to take snapshots periodically. + """ + + def __init__(self, tracker: 'ClassTracker', interval: float, *args: Any, + **kwargs: Any): + """ + Create thread with given interval and associated with the given + tracker. + """ + self.interval = interval + self.tracker = tracker + self.stop = False + super(PeriodicThread, self).__init__(*args, **kwargs) + + def run(self) -> None: + """ + Loop until a stop signal is set. + """ + self.stop = False + while not self.stop: + self.tracker.create_snapshot() + sleep(self.interval) + + +class Snapshot(object): + """Sample sizes of objects and the process at an instant.""" + + def __init__(self, timestamp: float, description: str = '') -> None: + """Initialize process-wide size information.""" + self.tracked_total = 0 + self.asizeof_total = 0 + self.overhead = 0 + self.timestamp = timestamp + self.system_total = pympler.process.ProcessMemoryInfo() + self.desc = description + self.classes = None # type: Optional[Dict[str, Dict[str, Any]]] + + @property + def total(self) -> int: + """ + Return the total (virtual) size of the process in bytes. If process + information is not available, get the best number available, even if it + is a poor approximation of reality. + """ + if self.system_total.available: + return self.system_total.vsz + elif self.asizeof_total: # pragma: no cover + return self.asizeof_total + else: # pragma: no cover + return self.tracked_total + + @property + def label(self) -> str: + """Return timestamped label for this snapshot, or a raw timestamp.""" + if not self.desc: + return "%.3fs" % self.timestamp + return "%s (%.3fs)" % (self.desc, self.timestamp) + + +class ClassTracker(object): + + def __init__(self, stream: Optional[IO] = None): + """ + Creates a new `ClassTracker` object. + + :param stream: Output stream to use when printing statistics via + ``stats``. + """ + # Dictionaries of TrackedObject objects associated with the actual + # objects that are tracked. 'index' uses the class name as the key and + # associates a list of tracked objects. It contains all TrackedObject + # instances, including those of dead objects. + self.index = defaultdict(list) # type: Dict[str, List[TrackedObject]] + + # 'objects' uses the id (address) as the key and associates the tracked + # object with it. TrackedObject's referring to dead objects are + # replaced lazily, i.e. when the id is recycled by another tracked + # object. + self.objects = {} # type: Dict[int, Any] + + # List of `Snapshot` objects. + self.snapshots = [] # type: List[Snapshot] + + # Time series of instance count for each tracked class. + self.history = defaultdict(list) \ + # type: Dict[str, List[Tuple[float, int]]] + + # Keep objects alive by holding a strong reference. + self._keepalive = [] # type: List[Any] + + # Dictionary of class observers identified by classname. + self._observers = {} # type: Dict[type, _ClassObserver] + + # Thread object responsible for background monitoring + self._periodic_thread = None # type: Optional[PeriodicThread] + + self._stream = stream + + @property + def stats(self) -> ConsoleStats: + """ + Return a ``ConsoleStats`` instance initialized with the current state + of the class tracker. + """ + return ConsoleStats(tracker=self, stream=self._stream) + + def _tracker(self, _observer_: _ClassObserver, _self_: Any, *args: Any, + **kwds: Any) -> None: + """ + Injected constructor for tracked classes. + Call the actual constructor of the object and track the object. Attach + to the object before calling the constructor to track the object with + the parameters of the most specialized class. + """ + self.track_object(_self_, + name=_observer_.name, + resolution_level=_observer_.detail, + keep=_observer_.keep, + trace=_observer_.trace) + _observer_.init(_self_, *args, **kwds) + + def _inject_constructor(self, cls: type, func: Callable, name: str, + resolution_level: int, keep: bool, trace: bool, + ) -> None: + """ + Modifying Methods in Place - after the recipe 15.7 in the Python + Cookbook by Ken Seehof. The original constructors may be restored + later. + """ + try: + constructor = cls.__init__ # type: ignore + except AttributeError: + def constructor(self: Any, *_args: Any, **_kwargs: Any) -> None: + pass + + # Possible name clash between keyword arguments of the tracked class' + # constructor and the curried arguments of the injected constructor. + # Therefore, the additional argument has a 'magic' name to make it less + # likely that an argument name clash occurs. + observer = _ClassObserver(constructor, + name, + resolution_level, + keep, + trace) + self._observers[cls] = observer + + def new_constructor(*args: Any, **kwargs: Any) -> None: + return func(observer, *args, **kwargs) + + cls.__init__ = new_constructor # type: ignore + + def _is_tracked(self, cls: type) -> bool: + """ + Determine if the class is tracked. + """ + return cls in self._observers + + def _track_modify(self, cls: type, name: str, detail: int, keep: bool, + trace: bool) -> None: + """ + Modify settings of a tracked class + """ + self._observers[cls].modify(name, detail, keep, trace) + + def _restore_constructor(self, cls: type) -> None: + """ + Restore the original constructor, lose track of class. + """ + cls.__init__ = self._observers[cls].init # type: ignore + del self._observers[cls] + + def track_change(self, instance: Any, resolution_level: int = 0) -> None: + """ + Change tracking options for the already tracked object 'instance'. + If instance is not tracked, a KeyError will be raised. + """ + tobj = self.objects[id(instance)] + tobj.set_resolution_level(resolution_level) + + def track_object(self, instance: Any, name: Optional[str] = None, + resolution_level: int = 0, keep: bool = False, + trace: bool = False) -> None: + """ + Track object 'instance' and sample size and lifetime information. Not + all objects can be tracked; trackable objects are class instances and + other objects that can be weakly referenced. When an object cannot be + tracked, a `TypeError` is raised. + + :param resolution_level: The recursion depth up to which referents are + sized individually. Resolution level 0 (default) treats the object + as an opaque entity, 1 sizes all direct referents individually, 2 + also sizes the referents of the referents and so forth. + :param keep: Prevent the object's deletion by keeping a (strong) + reference to the object. + """ + + # Check if object is already tracked. This happens if track_object is + # called multiple times for the same object or if an object inherits + # from multiple tracked classes. In the latter case, the most + # specialized class wins. To detect id recycling, the weak reference + # is checked. If it is 'None' a tracked object is dead and another one + # takes the same 'id'. + if id(instance) in self.objects and \ + self.objects[id(instance)].ref() is not None: + return + + name = name if name else instance.__class__.__name__ + + track_object_creation(self.history[name]) + on_delete = partial(track_object_deletion, self.history[name]) + + tobj = TrackedObject(instance, + name, + resolution_level=resolution_level, + trace=trace, + on_delete=on_delete) + + self.index[name].append(tobj) + self.objects[id(instance)] = tobj + + if keep: + self._keepalive.append(instance) + + def track_class(self, cls: type, name: Optional[str] = None, + resolution_level: int = 0, keep: bool = False, + trace: bool = False) -> None: + """ + Track all objects of the class `cls`. Objects of that type that already + exist are *not* tracked. If `track_class` is called for a class already + tracked, the tracking parameters are modified. Instantiation traces can + be generated by setting `trace` to True. + A constructor is injected to begin instance tracking on creation + of the object. The constructor calls `track_object` internally. + + :param cls: class to be tracked, may be an old-style or a new-style + class + :param name: reference the class by a name, default is the + concatenation of module and class name + :param resolution_level: The recursion depth up to which referents are + sized individually. Resolution level 0 (default) treats the object + as an opaque entity, 1 sizes all direct referents individually, 2 + also sizes the referents of the referents and so forth. + :param keep: Prevent the object's deletion by keeping a (strong) + reference to the object. + :param trace: Save instantiation stack trace for each instance + """ + if not isclass(cls): + raise TypeError("only class objects can be tracked") + if name is None: + name = cls.__module__ + '.' + cls.__name__ + if self._is_tracked(cls): + self._track_modify(cls, name, resolution_level, keep, trace) + else: + self._inject_constructor(cls, self._tracker, name, + resolution_level, keep, trace) + + def detach_class(self, cls: type) -> None: + """ + Stop tracking class 'cls'. Any new objects of that type are not + tracked anymore. Existing objects are still tracked. + """ + self._restore_constructor(cls) + + def detach_all_classes(self) -> None: + """ + Detach from all tracked classes. + """ + classes = list(self._observers.keys()) + for cls in classes: + self.detach_class(cls) + + def detach_all(self) -> None: + """ + Detach from all tracked classes and objects. + Restore the original constructors and cleanse the tracking lists. + """ + self.detach_all_classes() + self.objects.clear() + self.index.clear() + self._keepalive[:] = [] + + def clear(self) -> None: + """ + Clear all gathered data and detach from all tracked objects/classes. + """ + self.detach_all() + self.snapshots[:] = [] + + def close(self) -> None: + """ + Detach from tracked classes by removing injected constructors. Makes it + possible to use ClassTracker in `contextlib.closing` to safely remove + profiling hooks when the tracker goes out of scope:: + + import contextlib + with contextlib.closing(ClassTracker()) as tracker: + tracker.track_class(Foo) + + """ + self.detach_all_classes() + +# +# Background Monitoring +# + + def start_periodic_snapshots(self, interval: float = 1.0) -> None: + """ + Start a thread which takes snapshots periodically. The `interval` + specifies the time in seconds the thread waits between taking + snapshots. The thread is started as a daemon allowing the program to + exit. If periodic snapshots are already active, the interval is + updated. + """ + if not self._periodic_thread: + self._periodic_thread = PeriodicThread(self, interval, + name='BackgroundMonitor') + self._periodic_thread.setDaemon(True) + self._periodic_thread.start() + else: + self._periodic_thread.interval = interval + + def stop_periodic_snapshots(self) -> None: + """ + Post a stop signal to the thread that takes the periodic snapshots. The + function waits for the thread to terminate which can take some time + depending on the configured interval. + """ + if self._periodic_thread and self._periodic_thread.is_alive(): + self._periodic_thread.stop = True + self._periodic_thread.join() + self._periodic_thread = None + +# +# Snapshots +# + + snapshot_lock = Lock() + + def create_snapshot(self, description: str = '', + compute_total: bool = False) -> None: + """ + Collect current per instance statistics and saves total amount of + memory associated with the Python process. + + If `compute_total` is `True`, the total consumption of all objects + known to *asizeof* is computed. The latter might be very slow if many + objects are mapped into memory at the time the snapshot is taken. + Therefore, `compute_total` is set to `False` by default. + + The overhead of the `ClassTracker` structure is also computed. + + Snapshots can be taken asynchronously. The function is protected with a + lock to prevent race conditions. + """ + + try: + # TODO: It is not clear what happens when memory is allocated or + # released while this function is executed but it will likely lead + # to inconsistencies. Either pause all other threads or don't size + # individual objects in asynchronous mode. + self.snapshot_lock.acquire() + + timestamp = _get_time() + + sizer = asizeof.Asizer() + objs = [tobj.ref() for tobj in list(self.objects.values())] + sizer.exclude_refs(*objs) + + # The objects need to be sized in a deterministic order. Sort the + # objects by its creation date which should at least work for + # non-parallel execution. The "proper" fix would be to handle + # shared data separately. + tracked_objects = list(self.objects.values()) + tracked_objects.sort(key=lambda x: x.birth) + for tobj in tracked_objects: + tobj.track_size(timestamp, sizer) + + snapshot = Snapshot(timestamp, str(description)) + snapshot.tracked_total = sizer.total + if compute_total: + snapshot.asizeof_total = asizeof.asizeof(all=True, code=True) + + # Compute overhead of all structures, use sizer to exclude tracked + # objects(!) + snapshot.overhead = 0 + if snapshot.tracked_total: + snapshot.overhead = sizer.asizeof(self) + if snapshot.asizeof_total: + snapshot.asizeof_total -= snapshot.overhead + + self.snapshots.append(snapshot) + + finally: + self.snapshot_lock.release() diff --git a/venv/lib/python3.9/site-packages/pympler/classtracker_stats.py b/venv/lib/python3.9/site-packages/pympler/classtracker_stats.py new file mode 100644 index 00000000..191f1fba --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/classtracker_stats.py @@ -0,0 +1,780 @@ +""" +Provide saving, loading and presenting gathered `ClassTracker` statistics. +""" + +from typing import ( + Any, Dict, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING, Union +) + +import os +import pickle +import sys +from copy import deepcopy +from pympler.util.stringutils import trunc, pp, pp_timestamp + +from pympler.asizeof import Asized + +if TYPE_CHECKING: + from .classtracker import TrackedObject, ClassTracker, Snapshot + + +__all__ = ["Stats", "ConsoleStats", "HtmlStats"] + + +def _ref2key(ref: Asized) -> str: + return ref.name.split(':')[0] + + +def _merge_asized(base: Asized, other: Asized, level: int = 0) -> None: + """ + Merge **Asized** instances `base` and `other` into `base`. + """ + base.size += other.size + base.flat += other.flat + if level > 0: + base.name = _ref2key(base) + # Add refs from other to base. Any new refs are appended. + base.refs = list(base.refs) # we may need to append items + refs = {} + for ref in base.refs: + refs[_ref2key(ref)] = ref + for ref in other.refs: + key = _ref2key(ref) + if key in refs: + _merge_asized(refs[key], ref, level=level + 1) + else: + # Don't modify existing Asized instances => deepcopy + base.refs.append(deepcopy(ref)) + base.refs[-1].name = key + + +def _merge_objects(tref: float, merged: Asized, obj: 'TrackedObject') -> None: + """ + Merge the snapshot size information of multiple tracked objects. The + tracked object `obj` is scanned for size information at time `tref`. + The sizes are merged into **Asized** instance `merged`. + """ + size = None + for (timestamp, tsize) in obj.snapshots: + if timestamp == tref: + size = tsize + if size: + _merge_asized(merged, size) + + +def _format_trace(trace: List[Tuple]) -> str: + """ + Convert the (stripped) stack-trace to a nice readable format. The stack + trace `trace` is a list of frame records as returned by + **inspect.stack** but without the frame objects. + Returns a string. + """ + lines = [] + for fname, lineno, func, src, _ in trace: + if src: + for line in src: + lines.append(' ' + line.strip() + '\n') + lines.append(' %s:%4d in %s\n' % (fname, lineno, func)) + return ''.join(lines) + + +class Stats(object): + """ + Presents the memory statistics gathered by a `ClassTracker` based on user + preferences. + """ + + def __init__(self, tracker: 'Optional[ClassTracker]' = None, + filename: Optional[str] = None, + stream: Optional[IO] = None): + """ + Initialize the data log structures either from a `ClassTracker` + instance (argument `tracker`) or a previously dumped file (argument + `filename`). + + :param tracker: ClassTracker instance + :param filename: filename of previously dumped statistics + :param stream: where to print statistics, defaults to ``sys.stdout`` + """ + if stream: + self.stream = stream + else: + self.stream = sys.stdout + self.tracker = tracker + self.index = {} # type: Dict[str, List[TrackedObject]] + self.snapshots = [] # type: List[Snapshot] + if tracker: + self.index = tracker.index + self.snapshots = tracker.snapshots + self.history = tracker.history + self.sorted = [] # type: List[TrackedObject] + if filename: + self.load_stats(filename) + + def load_stats(self, fdump: Union[str, IO[bytes]]) -> None: + """ + Load the data from a dump file. + The argument `fdump` can be either a filename or an open file object + that requires read access. + """ + if isinstance(fdump, str): + fdump = open(fdump, 'rb') + self.index = pickle.load(fdump) + self.snapshots = pickle.load(fdump) + self.sorted = [] + + def dump_stats(self, fdump: Union[str, IO[bytes]], close: bool = True + ) -> None: + """ + Dump the logged data to a file. + The argument `file` can be either a filename or an open file object + that requires write access. `close` controls if the file is closed + before leaving this method (the default behaviour). + """ + if self.tracker: + self.tracker.stop_periodic_snapshots() + + if isinstance(fdump, str): + fdump = open(fdump, 'wb') + pickle.dump(self.index, fdump, protocol=pickle.HIGHEST_PROTOCOL) + pickle.dump(self.snapshots, fdump, protocol=pickle.HIGHEST_PROTOCOL) + if close: + fdump.close() + + def _init_sort(self) -> None: + """ + Prepare the data to be sorted. + If not yet sorted, import all tracked objects from the tracked index. + Extend the tracking information by implicit information to make + sorting easier (DSU pattern). + """ + if not self.sorted: + # Identify the snapshot that tracked the largest amount of memory. + tmax = None + maxsize = 0 + for snapshot in self.snapshots: + if snapshot.tracked_total > maxsize: + tmax = snapshot.timestamp + for key in list(self.index.keys()): + for tobj in self.index[key]: + tobj.classname = key # type: ignore + tobj.size = tobj.get_max_size() # type: ignore + tobj.tsize = tobj.get_size_at_time(tmax) # type: ignore + self.sorted.extend(self.index[key]) + + def sort_stats(self, *args: str) -> 'Stats': + """ + Sort the tracked objects according to the supplied criteria. The + argument is a string identifying the basis of a sort (example: 'size' + or 'classname'). When more than one key is provided, then additional + keys are used as secondary criteria when there is equality in all keys + selected before them. For example, ``sort_stats('name', 'size')`` will + sort all the entries according to their class name, and resolve all + ties (identical class names) by sorting by size. The criteria are + fields in the tracked object instances. Results are stored in the + ``self.sorted`` list which is used by ``Stats.print_stats()`` and other + methods. The fields available for sorting are: + + 'classname' + the name with which the class was registered + 'name' + the classname + 'birth' + creation timestamp + 'death' + destruction timestamp + 'size' + the maximum measured size of the object + 'tsize' + the measured size during the largest snapshot + 'repr' + string representation of the object + + Note that sorts on size are in descending order (placing most memory + consuming items first), whereas name, repr, and creation time searches + are in ascending order (alphabetical). + + The function returns self to allow calling functions on the result:: + + stats.sort_stats('size').reverse_order().print_stats() + """ + + criteria = ('classname', 'tsize', 'birth', 'death', + 'name', 'repr', 'size') + + if not set(criteria).issuperset(set(args)): + raise ValueError("Invalid sort criteria") + + if not args: + args = criteria + + def args_to_tuple(obj: 'TrackedObject') -> Tuple[str, ...]: + keys: List[str] = [] + for attr in args: + attribute = getattr(obj, attr, '') + if attr in ('tsize', 'size'): + attribute = -int(attribute) + keys.append(attribute) + return tuple(keys) + + self._init_sort() + self.sorted.sort(key=args_to_tuple) + + return self + + def reverse_order(self) -> 'Stats': + """ + Reverse the order of the tracked instance index `self.sorted`. + """ + self._init_sort() + self.sorted.reverse() + return self + + def annotate(self) -> None: + """ + Annotate all snapshots with class-based summaries. + """ + for snapshot in self.snapshots: + self.annotate_snapshot(snapshot) + + def annotate_snapshot(self, snapshot: 'Snapshot' + ) -> Dict[str, Dict[str, Any]]: + """ + Store additional statistical data in snapshot. + """ + if snapshot.classes is not None: + return snapshot.classes + + snapshot.classes = {} + + for classname in list(self.index.keys()): + total = 0 + active = 0 + merged = Asized(0, 0) + for tobj in self.index[classname]: + _merge_objects(snapshot.timestamp, merged, tobj) + total += tobj.get_size_at_time(snapshot.timestamp) + if (tobj.birth < snapshot.timestamp and + (tobj.death is None or + tobj.death > snapshot.timestamp)): + active += 1 + + try: + pct = total * 100.0 / snapshot.total + except ZeroDivisionError: # pragma: no cover + pct = 0 + try: + avg = total / active + except ZeroDivisionError: + avg = 0 + + snapshot.classes[classname] = dict(sum=total, + avg=avg, + pct=pct, + active=active) + snapshot.classes[classname]['merged'] = merged + + return snapshot.classes + + @property + def tracked_classes(self) -> List[str]: + """Return a list of all tracked classes occurring in any snapshot.""" + return sorted(list(self.index.keys())) + + +class ConsoleStats(Stats): + """ + Presentation layer for `Stats` to be used in text-based consoles. + """ + + def _print_refs(self, refs: Iterable[Asized], total: int, + prefix: str = ' ', level: int = 1, minsize: int = 0, + minpct: float = 0.1) -> None: + """ + Print individual referents recursively. + """ + lrefs = list(refs) + lrefs.sort(key=lambda x: x.size) + lrefs.reverse() + for ref in lrefs: + if ref.size > minsize and (ref.size * 100.0 / total) > minpct: + self.stream.write('%-50s %-14s %3d%% [%d]\n' % ( + trunc(prefix + str(ref.name), 50), + pp(ref.size), + int(ref.size * 100.0 / total), + level + )) + self._print_refs(ref.refs, total, prefix=prefix + ' ', + level=level + 1) + + def print_object(self, tobj: 'TrackedObject') -> None: + """ + Print the gathered information of object `tobj` in human-readable + format. + """ + if tobj.death: + self.stream.write('%-32s ( free ) %-35s\n' % ( + trunc(tobj.name, 32, left=True), trunc(tobj.repr, 35))) + else: + self.stream.write('%-32s 0x%08x %-35s\n' % ( + trunc(tobj.name, 32, left=True), + + tobj.id, + trunc(tobj.repr, 35) + )) + if tobj.trace: + self.stream.write(_format_trace(tobj.trace)) + for (timestamp, size) in tobj.snapshots: + self.stream.write(' %-30s %s\n' % ( + pp_timestamp(timestamp), pp(size.size) + )) + self._print_refs(size.refs, size.size) + if tobj.death is not None: + self.stream.write(' %-30s finalize\n' % ( + pp_timestamp(tobj.death), + )) + + def print_stats(self, clsname: Optional[str] = None, limit: float = 1.0 + ) -> None: + """ + Write tracked objects to stdout. The output can be filtered and + pruned. Only objects are printed whose classname contain the substring + supplied by the `clsname` argument. The output can be pruned by + passing a `limit` value. + + :param clsname: Only print objects whose classname contain the given + substring. + :param limit: If `limit` is a float smaller than one, only the supplied + percentage of the total tracked data is printed. If `limit` is + bigger than one, this number of tracked objects are printed. + Tracked objects are first filtered, and then pruned (if specified). + """ + if self.tracker: + self.tracker.stop_periodic_snapshots() + + if not self.sorted: + self.sort_stats() + + _sorted = self.sorted + + if clsname: + _sorted = [ + to for to in _sorted + if clsname in to.classname # type: ignore + ] + + if limit < 1.0: + limit = max(1, int(len(self.sorted) * limit)) + _sorted = _sorted[:int(limit)] + + # Emit per-instance data + for tobj in _sorted: + self.print_object(tobj) + + def print_summary(self) -> None: + """ + Print per-class summary for each snapshot. + """ + # Emit class summaries for each snapshot + classlist = self.tracked_classes + + fobj = self.stream + + fobj.write('---- SUMMARY ' + '-' * 66 + '\n') + for snapshot in self.snapshots: + classes = self.annotate_snapshot(snapshot) + fobj.write('%-35s %11s %12s %12s %5s\n' % ( + trunc(snapshot.desc, 35), + 'active', + pp(snapshot.asizeof_total), + 'average', + 'pct' + )) + for classname in classlist: + info = classes[classname] + fobj.write(' %-33s %11d %12s %12s %4d%%\n' % ( + trunc(classname, 33), + info['active'], + pp(info['sum']), + pp(info['avg']), + info['pct'] + )) + fobj.write('-' * 79 + '\n') + + +class HtmlStats(Stats): + """ + Output the `ClassTracker` statistics as HTML pages and graphs. + """ + + style = """<style type="text/css"> + table { width:100%; border:1px solid #000; border-spacing:0px; } + td, th { border:0px; } + div { width:200px; padding:10px; background-color:#FFEECC; } + #nb { border:0px; } + #tl { margin-top:5mm; margin-bottom:5mm; } + #p1 { padding-left: 5px; } + #p2 { padding-left: 50px; } + #p3 { padding-left: 100px; } + #p4 { padding-left: 150px; } + #p5 { padding-left: 200px; } + #p6 { padding-left: 210px; } + #p7 { padding-left: 220px; } + #hl { background-color:#FFFFCC; } + #r1 { background-color:#BBBBBB; } + #r2 { background-color:#CCCCCC; } + #r3 { background-color:#DDDDDD; } + #r4 { background-color:#EEEEEE; } + #r5,#r6,#r7 { background-color:#FFFFFF; } + #num { text-align:right; } + </style> + """ + + nopylab_msg = """<div color="#FFCCCC">Could not generate %s chart! + Install <a href="http://matplotlib.sourceforge.net/">Matplotlib</a> + to generate charts.</div>\n""" + + chart_tag = '<img src="%s">\n' + header = "<html><head><title>%s</title>%s</head><body>\n" + tableheader = '<table border="1">\n' + tablefooter = '</table>\n' + footer = '</body></html>\n' + + refrow = """<tr id="r%(level)d"> + <td id="p%(level)d">%(name)s</td> + <td id="num">%(size)s</td> + <td id="num">%(pct)3.1f%%</td></tr>""" + + def _print_refs(self, fobj: IO, refs: Iterable[Asized], total: int, + level: int = 1, minsize: int = 0, minpct: float = 0.1 + ) -> None: + """ + Print individual referents recursively. + """ + lrefs = list(refs) + lrefs.sort(key=lambda x: x.size) + lrefs.reverse() + if level == 1: + fobj.write('<table>\n') + for ref in lrefs: + if ref.size > minsize and (ref.size * 100.0 / total) > minpct: + data = dict(level=level, + name=trunc(str(ref.name), 128), + size=pp(ref.size), + pct=ref.size * 100.0 / total) + fobj.write(self.refrow % data) + self._print_refs(fobj, ref.refs, total, level=level + 1) + if level == 1: + fobj.write("</table>\n") + + class_summary = """<p>%(cnt)d instances of %(cls)s were registered. The + average size is %(avg)s, the minimal size is %(min)s, the maximum size + is %(max)s.</p>\n""" + class_snapshot = '''<h3>Snapshot: %(name)s, %(total)s occupied by instances + of class %(cls)s</h3>\n''' + + def print_class_details(self, fname: str, classname: str) -> None: + """ + Print detailed statistics and instances for the class `classname`. All + data will be written to the file `fname`. + """ + fobj = open(fname, "w") + fobj.write(self.header % (classname, self.style)) + + fobj.write("<h1>%s</h1>\n" % (classname)) + + sizes = [tobj.get_max_size() for tobj in self.index[classname]] + total = 0 + for s in sizes: + total += s + data = {'cnt': len(self.index[classname]), 'cls': classname} + data['avg'] = pp(total / len(sizes)) + data['max'] = pp(max(sizes)) + data['min'] = pp(min(sizes)) + fobj.write(self.class_summary % data) + + fobj.write(self.charts[classname]) + + fobj.write("<h2>Coalesced Referents per Snapshot</h2>\n") + for snapshot in self.snapshots: + if snapshot.classes and classname in snapshot.classes: + merged = snapshot.classes[classname]['merged'] + fobj.write(self.class_snapshot % { + 'name': snapshot.desc, + 'cls': classname, + 'total': pp(merged.size), + }) + if merged.refs: + self._print_refs(fobj, merged.refs, merged.size) + else: + fobj.write('<p>No per-referent sizes recorded.</p>\n') + + fobj.write("<h2>Instances</h2>\n") + for tobj in self.index[classname]: + fobj.write('<table id="tl" width="100%" rules="rows">\n') + fobj.write('<tr><td id="hl" width="140px">Instance</td>' + + '<td id="hl">%s at 0x%08x</td></tr>\n' % + (tobj.name, tobj.id)) + if tobj.repr: + fobj.write("<tr><td>Representation</td>" + + "<td>%s </td></tr>\n" % tobj.repr) + fobj.write("<tr><td>Lifetime</td><td>%s - %s</td></tr>\n" % + (pp_timestamp(tobj.birth), pp_timestamp(tobj.death))) + if tobj.trace: + trace = "<pre>%s</pre>" % (_format_trace(tobj.trace)) + fobj.write("<tr><td>Instantiation</td><td>%s</td></tr>\n" % + trace) + for (timestamp, size) in tobj.snapshots: + fobj.write("<tr><td>%s</td>" % pp_timestamp(timestamp)) + if not size.refs: + fobj.write("<td>%s</td></tr>\n" % pp(size.size)) + else: + fobj.write("<td>%s" % pp(size.size)) + self._print_refs(fobj, size.refs, size.size) + fobj.write("</td></tr>\n") + fobj.write("</table>\n") + + fobj.write(self.footer) + fobj.close() + + snapshot_cls_header = """<tr> + <th id="hl">Class</th> + <th id="hl" align="right">Instance #</th> + <th id="hl" align="right">Total</th> + <th id="hl" align="right">Average size</th> + <th id="hl" align="right">Share</th></tr>\n""" + + snapshot_cls = """<tr> + <td>%(cls)s</td> + <td align="right">%(active)d</td> + <td align="right">%(sum)s</td> + <td align="right">%(avg)s</td> + <td align="right">%(pct)3.2f%%</td></tr>\n""" + + snapshot_summary = """<p>Total virtual memory assigned to the program + at that time was %(sys)s, which includes %(overhead)s profiling + overhead. The ClassTracker tracked %(tracked)s in total. The measurable + objects including code objects but excluding overhead have a total size + of %(asizeof)s.</p>\n""" + + def relative_path(self, filepath: str, basepath: Optional[str] = None + ) -> str: + """ + Convert the filepath path to a relative path against basepath. By + default basepath is self.basedir. + """ + if basepath is None: + basepath = self.basedir + if not basepath: + return filepath + if filepath.startswith(basepath): + filepath = filepath[len(basepath):] + if filepath and filepath[0] == os.sep: + filepath = filepath[1:] + return filepath + + def create_title_page(self, filename: str, title: str = '') -> None: + """ + Output the title page. + """ + fobj = open(filename, "w") + fobj.write(self.header % (title, self.style)) + + fobj.write("<h1>%s</h1>\n" % title) + fobj.write("<h2>Memory distribution over time</h2>\n") + fobj.write(self.charts['snapshots']) + + fobj.write("<h2>Snapshots statistics</h2>\n") + fobj.write('<table id="nb">\n') + + classlist = list(self.index.keys()) + classlist.sort() + + for snapshot in self.snapshots: + fobj.write('<tr><td>\n') + fobj.write('<table id="tl" rules="rows">\n') + fobj.write("<h3>%s snapshot at %s</h3>\n" % ( + snapshot.desc or 'Untitled', + pp_timestamp(snapshot.timestamp) + )) + + data = {} + data['sys'] = pp(snapshot.system_total.vsz) + data['tracked'] = pp(snapshot.tracked_total) + data['asizeof'] = pp(snapshot.asizeof_total) + data['overhead'] = pp(getattr(snapshot, 'overhead', 0)) + + fobj.write(self.snapshot_summary % data) + + if snapshot.tracked_total: + fobj.write(self.snapshot_cls_header) + for classname in classlist: + if snapshot.classes: + info = snapshot.classes[classname].copy() + path = self.relative_path(self.links[classname]) + info['cls'] = '<a href="%s">%s</a>' % (path, classname) + info['sum'] = pp(info['sum']) + info['avg'] = pp(info['avg']) + fobj.write(self.snapshot_cls % info) + fobj.write('</table>') + fobj.write('</td><td>\n') + if snapshot.tracked_total: + fobj.write(self.charts[snapshot]) + fobj.write('</td></tr>\n') + + fobj.write("</table>\n") + fobj.write(self.footer) + fobj.close() + + def create_lifetime_chart(self, classname: str, filename: str = '') -> str: + """ + Create chart that depicts the lifetime of the instance registered with + `classname`. The output is written to `filename`. + """ + try: + from pylab import figure, title, xlabel, ylabel, plot, savefig + except ImportError: + return HtmlStats.nopylab_msg % (classname + " lifetime") + + cnt = [] + for tobj in self.index[classname]: + cnt.append([tobj.birth, 1]) + if tobj.death: + cnt.append([tobj.death, -1]) + cnt.sort() + for i in range(1, len(cnt)): + cnt[i][1] += cnt[i - 1][1] + + x = [t for [t, c] in cnt] + y = [c for [t, c] in cnt] + + figure() + xlabel("Execution time [s]") + ylabel("Instance #") + title("%s instances" % classname) + plot(x, y, 'o') + savefig(filename) + + return self.chart_tag % (os.path.basename(filename)) + + def create_snapshot_chart(self, filename: str = '') -> str: + """ + Create chart that depicts the memory allocation over time apportioned + to the tracked classes. + """ + try: + from pylab import (figure, title, xlabel, ylabel, plot, fill, + legend, savefig) + import matplotlib.mlab as mlab + except ImportError: + return self.nopylab_msg % ("memory allocation") + + classlist = self.tracked_classes + + times = [snapshot.timestamp for snapshot in self.snapshots] + base = [0.0] * len(self.snapshots) + poly_labels = [] + polys = [] + for cn in classlist: + pct = [snapshot.classes[cn]['pct'] for snapshot in self.snapshots + if snapshot.classes is not None] + if pct and max(pct) > 3.0: + sz = [float(fp.classes[cn]['sum']) / (1024 * 1024) + for fp in self.snapshots + if fp.classes is not None] + sz = [sx + sy for sx, sy in zip(base, sz)] + xp, yp = mlab.poly_between(times, base, sz) + polys.append(((xp, yp), {'label': cn})) + poly_labels.append(cn) + base = sz + + figure() + title("Snapshot Memory") + xlabel("Execution Time [s]") + ylabel("Virtual Memory [MiB]") + + sizes = [float(fp.asizeof_total) / (1024 * 1024) + for fp in self.snapshots] + plot(times, sizes, 'r--', label='Total') + sizes = [float(fp.tracked_total) / (1024 * 1024) + for fp in self.snapshots] + plot(times, sizes, 'b--', label='Tracked total') + + for (args, kwds) in polys: + fill(*args, **kwds) + legend(loc=2) + savefig(filename) + + return self.chart_tag % (self.relative_path(filename)) + + def create_pie_chart(self, snapshot: 'Snapshot', filename: str = '') -> str: + """ + Create a pie chart that depicts the distribution of the allocated + memory for a given `snapshot`. The chart is saved to `filename`. + """ + try: + from pylab import figure, title, pie, axes, savefig + from pylab import sum as pylab_sum + except ImportError: + return self.nopylab_msg % ("pie_chart") + + # Don't bother illustrating a pie without pieces. + if not snapshot.tracked_total or snapshot.classes is None: + return '' + + classlist = [] + sizelist = [] + for k, v in list(snapshot.classes.items()): + if v['pct'] > 3.0: + classlist.append(k) + sizelist.append(v['sum']) + sizelist.insert(0, snapshot.asizeof_total - pylab_sum(sizelist)) + classlist.insert(0, 'Other') + + title("Snapshot (%s) Memory Distribution" % (snapshot.desc)) + figure(figsize=(8, 8)) + axes([0.1, 0.1, 0.8, 0.8]) + pie(sizelist, labels=classlist) + savefig(filename, dpi=50) + + return self.chart_tag % (self.relative_path(filename)) + + def create_html(self, fname: str, title: str = "ClassTracker Statistics" + ) -> None: + """ + Create HTML page `fname` and additional files in a directory derived + from `fname`. + """ + # Create a folder to store the charts and additional HTML files. + self.basedir = os.path.dirname(os.path.abspath(fname)) + self.filesdir = os.path.splitext(fname)[0] + '_files' + if not os.path.isdir(self.filesdir): + os.mkdir(self.filesdir) + self.filesdir = os.path.abspath(self.filesdir) + self.links = {} # type: Dict[str, str] + + # Annotate all snapshots in advance + self.annotate() + + # Create charts. The tags to show the images are returned and stored in + # the self.charts dictionary. This allows to return alternative text if + # the chart creation framework is not available. + self.charts = {} # type: Dict[Union[str, Snapshot], str] + fn = os.path.join(self.filesdir, 'timespace.png') + self.charts['snapshots'] = self.create_snapshot_chart(fn) + + for fp, idx in zip(self.snapshots, list(range(len(self.snapshots)))): + fn = os.path.join(self.filesdir, 'fp%d.png' % (idx)) + self.charts[fp] = self.create_pie_chart(fp, fn) + + for cn in list(self.index.keys()): + fn = os.path.join(self.filesdir, cn.replace('.', '_') + '-lt.png') + self.charts[cn] = self.create_lifetime_chart(cn, fn) + + # Create HTML pages first for each class and then the index page. + for cn in list(self.index.keys()): + fn = os.path.join(self.filesdir, cn.replace('.', '_') + '.html') + self.links[cn] = fn + self.print_class_details(fn, cn) + + self.create_title_page(fname, title=title) diff --git a/venv/lib/python3.9/site-packages/pympler/garbagegraph.py b/venv/lib/python3.9/site-packages/pympler/garbagegraph.py new file mode 100644 index 00000000..fdd6cb74 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/garbagegraph.py @@ -0,0 +1,80 @@ + +from pympler.refgraph import ReferenceGraph +from pympler.util.stringutils import trunc, pp + +import sys +import gc + +__all__ = ['GarbageGraph', 'start_debug_garbage', 'end_debug_garbage'] + + +class GarbageGraph(ReferenceGraph): + """ + The ``GarbageGraph`` is a ``ReferenceGraph`` that illustrates the objects + building reference cycles. The garbage collector is switched to debug mode + (all identified garbage is stored in `gc.garbage`) and the garbage + collector is invoked. The collected objects are then illustrated in a + directed graph. + + Large graphs can be reduced to the actual cycles by passing ``reduce=True`` + to the constructor. + + It is recommended to disable the garbage collector when using the + ``GarbageGraph``. + + >>> from pympler.garbagegraph import GarbageGraph, start_debug_garbage + >>> start_debug_garbage() + >>> l = [] + >>> l.append(l) + >>> del l + >>> gb = GarbageGraph() + >>> gb.render('garbage.eps') + True + """ + def __init__(self, reduce=False, collectable=True): + """ + Initialize the GarbageGraph with the objects identified by the garbage + collector. If `collectable` is true, every reference cycle is recorded. + Otherwise only uncollectable objects are reported. + """ + if collectable: + gc.set_debug(gc.DEBUG_SAVEALL) + else: + gc.set_debug(0) + gc.collect() + + ReferenceGraph.__init__(self, gc.garbage, reduce) + + def print_stats(self, stream=None): + """ + Log annotated garbage objects to console or file. + + :param stream: open file, uses sys.stdout if not given + """ + if not stream: # pragma: no cover + stream = sys.stdout + self.metadata.sort(key=lambda x: -x.size) + stream.write('%-10s %8s %-12s %-46s\n' % ('id', 'size', 'type', + 'representation')) + for g in self.metadata: + stream.write('0x%08x %8d %-12s %-46s\n' % (g.id, g.size, + trunc(g.type, 12), + trunc(g.str, 46))) + stream.write('Garbage: %8d collected objects (%s in cycles): %12s\n' % + (self.count, self.num_in_cycles, pp(self.total_size))) + + +def start_debug_garbage(): + """ + Turn off garbage collector to analyze *collectable* reference cycles. + """ + gc.collect() + gc.disable() + + +def end_debug_garbage(): + """ + Turn garbage collection on and disable debug output. + """ + gc.set_debug(0) + gc.enable() diff --git a/venv/lib/python3.9/site-packages/pympler/mprofile.py b/venv/lib/python3.9/site-packages/pympler/mprofile.py new file mode 100644 index 00000000..936d6f76 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/mprofile.py @@ -0,0 +1,97 @@ +""" +Memory usage profiler for Python. + +""" +import inspect +import sys + +from pympler import muppy + + +class MProfiler(object): + """A memory usage profiler class. + + Memory data for each function is stored as a 3-element list in the + dictionary self.memories. The index is always a codepoint (see below). + The following are the definitions of the members: + + [0] = The number of times this function was called + [1] = Minimum memory consumption when this function was measured. + [2] = Maximum memory consumption when this function was measured. + + A codepoint is a list of 3-tuple of the type + (filename, functionname, linenumber). You can omit either element, which + will cause the profiling to be triggered if any of the other criteria + match. E.g. + - (None, foo, None), will profile any foo function, + - (bar, foo, None) will profile only the foo function from the bar file, + - (bar, foo, 17) will profile only line 17 of the foo function defined + in the file bar. + + Additionally, you can define on what events you want the profiling be + triggered. Possible events are defined in + http://docs.python.org/lib/debugger-hooks.html. + + If you do not define either codepoints or events, the profiler will + record the memory usage in at every codepoint and event. + + """ + + def __init__(self, codepoints=None, events=None): + """ + keyword arguments: + codepoints -- a list of points in code to monitor (defaults to all + codepoints) + events -- a list of events to monitor (defaults to all events) + """ + self.memories = {} + self.codepoints = codepoints + self.events = events + + def codepoint_included(self, codepoint): + """Check if codepoint matches any of the defined codepoints.""" + if self.codepoints is None: + return True + for cp in self.codepoints: + mismatch = False + for i in range(len(cp)): + if (cp[i] is not None) and (cp[i] != codepoint[i]): + mismatch = True + break + if not mismatch: + return True + return False + + def profile(self, frame, event, arg): # arg req to match signature + """Profiling method used to profile matching codepoints and events.""" + if (self.events is None) or (event in self.events): + frame_info = inspect.getframeinfo(frame) + cp = (frame_info[0], frame_info[2], frame_info[1]) + if self.codepoint_included(cp): + objects = muppy.get_objects() + size = muppy.get_size(objects) + if cp not in self.memories: + self.memories[cp] = [0, 0, 0, 0] + self.memories[cp][0] = 1 + self.memories[cp][1] = size + self.memories[cp][2] = size + else: + self.memories[cp][0] += 1 + if self.memories[cp][1] > size: + self.memories[cp][1] = size + if self.memories[cp][2] < size: + self.memories[cp][2] = size + + def run(self, cmd): + sys.setprofile(self.profile) + try: + exec(cmd) + finally: + sys.setprofile(None) + return self + + +if __name__ == "__main__": + p = MProfiler() + p.run("print('hello')") + print(p.memories) diff --git a/venv/lib/python3.9/site-packages/pympler/muppy.py b/venv/lib/python3.9/site-packages/pympler/muppy.py new file mode 100644 index 00000000..57000ff8 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/muppy.py @@ -0,0 +1,275 @@ +from typing import Any, Callable, Dict, List, Optional, Set, Tuple + +import gc + +from pympler import summary +from pympler.util import compat + +from inspect import isframe, stack + +from sys import getsizeof + +from pympler.asizeof import _Py_TPFLAGS_HAVE_GC + + +def ignore_object(obj: Any) -> bool: + try: + return isframe(obj) + except ReferenceError: + return True + + +def get_objects(remove_dups: bool = True, include_frames: bool = False + ) -> List[Any]: + """Return a list of all known objects excluding frame objects. + + If (outer) frame objects shall be included, pass `include_frames=True`. In + order to prevent building reference cycles, the current frame object (of + the caller of get_objects) is ignored. This will not prevent creating + reference cycles if the object list is passed up the call-stack. Therefore, + frame objects are not included by default. + + Keyword arguments: + remove_dups -- if True, all duplicate objects will be removed. + include_frames -- if True, includes frame objects. + """ + gc.collect() + + # Do not initialize local variables before calling gc.get_objects or those + # will be included in the list. Furthermore, ignore frame objects to + # prevent reference cycles. + tmp = gc.get_objects() + tmp = [o for o in tmp if not ignore_object(o)] + + res = [] + for o in tmp: + # gc.get_objects returns only container objects, but we also want + # the objects referenced by them + refs = get_referents(o) + for ref in refs: + if not gc.is_tracked(ref): + # we already got the container objects, now we only add + # non-container objects + res.append(ref) + res.extend(tmp) + if remove_dups: + res = _remove_duplicates(res) + + if include_frames: + for sf in stack()[2:]: + res.append(sf[0]) + return res + + +def get_size(objects: List[Any]) -> int: + """Compute the total size of all elements in objects.""" + res = 0 + for o in objects: + try: + res += getsizeof(o) + except AttributeError: + print("IGNORING: type=%s; o=%s" % (str(type(o)), str(o))) + return res + + +def get_diff(left: List[Any], right: List[Any]) -> Dict[str, List[Any]]: + """Get the difference of both lists. + + The result will be a dict with this form {'+': [], '-': []}. + Items listed in '+' exist only in the right list, + items listed in '-' exist only in the left list. + + """ + res = {'+': [], '-': []} # type: Dict[str, List[Any]] + + def partition(objects: List[Any]) -> Dict[type, List[Any]]: + """Partition the passed object list.""" + res = {} # type: Dict[type, List[Any]] + for o in objects: + t = type(o) + if type(o) not in res: + res[t] = [] + res[t].append(o) + return res + + def get_not_included(foo: List[Any], bar: Dict[type, List[Any]] + ) -> List[Any]: + """Compare objects from foo with objects defined in the values of + bar (set of partitions). + Returns a list of all objects included in list, but not dict values. + """ + res = [] # type: List[Any] + for o in foo: + if not compat.object_in_list(type(o), bar): + res.append(o) + elif not compat.object_in_list(o, bar[type(o)]): + res.append(o) + return res + + # Create partitions of both lists. This will reduce the time required for + # the comparison + left_objects = partition(left) + right_objects = partition(right) + # and then do the diff + res['+'] = get_not_included(right, left_objects) + res['-'] = get_not_included(left, right_objects) + return res + + +def sort(objects: List[Any]) -> List[Any]: + """Sort objects by size in bytes.""" + objects = sorted(objects, key=getsizeof) + return objects + + +def filter(objects: List[Any], Type: Optional[type] = None, min: int = -1, + max: int = -1) -> List[Any]: + """Filter objects. + + The filter can be by type, minimum size, and/or maximum size. + + Keyword arguments: + Type -- object type to filter by + min -- minimum object size + max -- maximum object size + + """ + res = [] # type: List[Any] + if min > max and max > -1: + raise ValueError("minimum must be smaller than maximum") + + if Type is not None: + objects = [o for o in objects if isinstance(o, Type)] + if min > -1: + objects = [o for o in objects if getsizeof(o) > min] + if max > -1: + objects = [o for o in objects if getsizeof(o) < max] + return objects + + +def get_referents(object: Any, level: int = 1) -> List[Any]: + """Get all referents of an object up to a certain level. + + The referents will not be returned in a specific order and + will not contain duplicate objects. Duplicate objects will be removed. + + Keyword arguments: + level -- level of indirection to which referents considered. + + This function is recursive. + + """ + res = gc.get_referents(object) + level -= 1 + if level > 0: + for o in res: + res.extend(get_referents(o, level)) + res = _remove_duplicates(res) + return res + + +def _get_usage(function: Callable, *args: Any) -> Optional[List]: + """Test if more memory is used after the function has been called. + + The function will be invoked twice and only the second measurement will be + considered. Thus, memory used in initialisation (e.g. loading modules) + will not be included in the result. The goal is to identify memory leaks + caused by functions which use more and more memory. + + Any arguments next to the function will be passed on to the function + on invocation. + + Note that this function is currently experimental, because it is not + tested thoroughly and performs poorly. + + """ + # The usage of a function is calculated by creating one summary of all + # objects before the function is invoked and afterwards. These summaries + # are compared and the diff is returned. + # This function works in a 2-steps process. Before the actual function is + # invoked an empty dummy function is measurement to identify the overhead + # involved in the measuring process. This overhead then is subtracted from + # the measurement performed on the passed function. The result reflects the + # actual usage of a function call. + # Also, a measurement is performed twice, allowing the adjustment to + # initializing things, e.g. modules + + res = None + + def _get_summaries(function: Callable, *args: Any) -> Tuple: + """Get a 2-tuple containing one summary from before, and one summary + from after the function has been invoked. + + """ + s_before = summary.summarize(get_objects()) + function(*args) + s_after = summary.summarize(get_objects()) + return (s_before, s_after) + + def _get_usage(function: Callable, *args: Any) -> List: + """Get the usage of a function call. + This function is to be used only internally. The 'real' get_usage + function is a wrapper around _get_usage, but the workload is done + here. + + """ + # init before calling + (s_before, s_after) = _get_summaries(function, *args) + # ignore all objects used for the measurement + ignore = [] + if s_before != s_after: + ignore.append(s_before) + for row in s_before: + # ignore refs from summary and frame (loop) + if len(gc.get_referrers(row)) == 2: + ignore.append(row) + for item in row: + # ignore refs from summary and frame (loop) + if len(gc.get_referrers(item)) == 2: + ignore.append(item) + for o in ignore: + s_after = summary._subtract(s_after, o) + res = summary.get_diff(s_before, s_after) + return summary._sweep(res) + + # calibrate; twice for initialization + def noop() -> None: + pass + offset = _get_usage(noop) + offset = _get_usage(noop) + # perform operation twice to handle objects possibly used in + # initialisation + tmp = _get_usage(function, *args) + tmp = _get_usage(function, *args) + tmp = summary.get_diff(offset, tmp) + tmp = summary._sweep(tmp) + if len(tmp) != 0: + res = tmp + return res + + +def _is_containerobject(o: Any) -> bool: + """Is the passed object a container object.""" + return bool(getattr(type(o), '__flags__', 0) & _Py_TPFLAGS_HAVE_GC) + + +def _remove_duplicates(objects: List[Any]) -> List[Any]: + """Remove duplicate objects. + + Inspired by http://www.peterbe.com/plog/uniqifiers-benchmark + + """ + seen = set() # type: Set[int] + result = [] + for item in objects: + marker = id(item) + if marker in seen: + continue + seen.add(marker) + result.append(item) + return result + + +def print_summary() -> None: + """Print a summary of all known objects.""" + summary.print_(summary.summarize(get_objects())) diff --git a/venv/lib/python3.9/site-packages/pympler/panels.py b/venv/lib/python3.9/site-packages/pympler/panels.py new file mode 100644 index 00000000..3bb6217a --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/panels.py @@ -0,0 +1,115 @@ +""" +Expose a memory-profiling panel to the Django Debug toolbar. + +Shows process memory information (virtual size, resident set size) and model +instances for the current request. + +Requires Django and Django Debug toolbar: + +https://github.com/jazzband/django-debug-toolbar + +Pympler adds a memory panel as a third party addon (not included in the +django-debug-toolbar). It can be added by overriding the `DEBUG_TOOLBAR_PANELS` +setting in the Django project settings:: + + DEBUG_TOOLBAR_PANELS = ( + 'debug_toolbar.panels.timer.TimerDebugPanel', + 'pympler.panels.MemoryPanel', + ) + +Pympler also needs to be added to the `INSTALLED_APPS` in the Django settings:: + + INSTALLED_APPS = INSTALLED_APPS + ('debug_toolbar', 'pympler') +""" + +from pympler.classtracker import ClassTracker +from pympler.process import ProcessMemoryInfo +from pympler.util.stringutils import pp + +try: + from debug_toolbar.panels import Panel + from django.apps import apps + from django.template import Context, Template + from django.template.loader import render_to_string + from django.http.request import HttpRequest + from django.http.response import HttpResponse +except ImportError: + class Panel(object): # type: ignore + pass + + class Template(object): # type: ignore + pass + + class Context(object): # type: ignore + pass + + class HttpRequest(object): # type: ignore + pass + + class HttpResponse(object): # type: ignore + pass + + +class MemoryPanel(Panel): + + name = 'pympler' + + title = 'Memory' + + template = 'memory_panel.html' + + classes = [Context, Template] + + def process_request(self, request: HttpRequest) -> HttpResponse: + self._tracker = ClassTracker() + for cls in apps.get_models() + self.classes: + self._tracker.track_class(cls) + self._tracker.create_snapshot('before') + self.record_stats({'before': ProcessMemoryInfo()}) + response = super(MemoryPanel, self).process_request(request) + self.record_stats({'after': ProcessMemoryInfo()}) + self._tracker.create_snapshot('after') + stats = self._tracker.stats + stats.annotate() + self.record_stats({'stats': stats}) + return response + + def enable_instrumentation(self) -> None: + self._tracker = ClassTracker() + for cls in apps.get_models() + self.classes: + self._tracker.track_class(cls) + + def disable_instrumentation(self) -> None: + self._tracker.detach_all_classes() + + def nav_subtitle(self) -> str: + context = self.get_stats() + before = context['before'] + after = context['after'] + rss = after.rss + delta = rss - before.rss + delta = ('(+%s)' % pp(delta)) if delta > 0 else '' + return "%s %s" % (pp(rss), delta) + + @property + def content(self) -> str: + context = self.get_stats() + before = context['before'] + after = context['after'] + stats = context['stats'] + rows = [('Resident set size', after.rss), + ('Virtual size', after.vsz), + ] + rows.extend(after - before) + rows = [(key, pp(value)) for key, value in rows] + rows.extend(after.os_specific) + + classes = [] + snapshot = stats.snapshots[-1] + for model in stats.tracked_classes: + history = [cnt for _, cnt in stats.history[model]] + size = snapshot.classes.get(model, {}).get('sum', 0) + if history and history[-1] > 0: + classes.append((model, history, pp(size))) + context.update({'rows': rows, 'classes': classes}) + return render_to_string(self.template, context) diff --git a/venv/lib/python3.9/site-packages/pympler/process.py b/venv/lib/python3.9/site-packages/pympler/process.py new file mode 100644 index 00000000..b3a31528 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/process.py @@ -0,0 +1,238 @@ +""" +This module queries process memory allocation metrics from the operating +system. It provides a platform independent layer to get the amount of virtual +and physical memory allocated to the Python process. + +Different mechanisms are implemented: Either the process stat file is read +(Linux), the `ps` command is executed (BSD/OSX/Solaris) or the resource module +is queried (Unix fallback). On Windows try to use the win32 module if +available. If all fails, return 0 for each attribute. + +Windows without the win32 module is not supported. + + >>> from pympler.process import ProcessMemoryInfo + >>> pmi = ProcessMemoryInfo() + >>> print ("Virtual size [Byte]: " + str(pmi.vsz)) # doctest: +ELLIPSIS + Virtual size [Byte]: ... +""" + +from typing import Iterable, List, Tuple + +import logging +import threading + +from mmap import PAGESIZE # type: ignore +from os import getpid +from subprocess import Popen, PIPE + +from pympler.util.stringutils import pp + + +class _ProcessMemoryInfo(object): + """Stores information about various process-level memory metrics. The + virtual size is stored in attribute `vsz`, the physical memory allocated to + the process in `rss`, and the number of (major) pagefaults in `pagefaults`. + On Linux, `data_segment`, `code_segment`, `shared_segment` and + `stack_segment` contain the number of Bytes allocated for the respective + segments. This is an abstract base class which needs to be overridden by + operating system specific implementations. This is done when importing the + module. + """ + + pagesize = PAGESIZE + + def __init__(self) -> None: + self.pid = getpid() + self.rss = 0 + self.vsz = 0 + self.pagefaults = 0 + self.os_specific = [] # type: List[Tuple[str, str]] + + self.data_segment = 0 + self.code_segment = 0 + self.shared_segment = 0 + self.stack_segment = 0 + + self.available = self.update() + + def __repr__(self) -> str: + return "<%s vsz=%d rss=%d>" % (self.__class__.__name__, + self.vsz, self.rss) + + def update(self) -> bool: + """ + Refresh the information using platform instruments. Returns true if + this operation yields useful values on the current platform. + """ + return False # pragma: no cover + + def __sub__(self, other: '_ProcessMemoryInfo') -> Iterable[Tuple[str, int]]: + diff = [('Resident set size (delta)', self.rss - other.rss), + ('Virtual size (delta)', self.vsz - other.vsz), + ] + return diff + + +ProcessMemoryInfo = _ProcessMemoryInfo # type: type + + +def is_available() -> bool: + """ + Convenience function to check if the current platform is supported by this + module. + """ + return ProcessMemoryInfo().update() + + +class _ProcessMemoryInfoPS(_ProcessMemoryInfo): + + def update(self) -> bool: + """ + Get virtual and resident size of current process via 'ps'. + This should work for MacOS X, Solaris, Linux. Returns true if it was + successful. + """ + try: + p = Popen(['/bin/ps', '-p%s' % self.pid, '-o', 'rss,vsz'], + stdout=PIPE, stderr=PIPE) + except OSError: # pragma: no cover + pass + else: + s = p.communicate()[0].split() + if p.returncode == 0 and len(s) >= 2: # pragma: no branch + self.vsz = int(s[-1]) * 1024 + self.rss = int(s[-2]) * 1024 + return True + return False # pragma: no cover + + +class _ProcessMemoryInfoProc(_ProcessMemoryInfo): + + key_map = { + 'VmPeak': 'Peak virtual memory size', + 'VmSize': 'Virtual memory size', + 'VmLck': 'Locked memory size', + 'VmHWM': 'Peak resident set size', + 'VmRSS': 'Resident set size', + 'VmStk': 'Size of stack segment', + 'VmData': 'Size of data segment', + 'VmExe': 'Size of code segment', + 'VmLib': 'Shared library code size', + 'VmPTE': 'Page table entries size', + } + + def update(self) -> bool: + """ + Get virtual size of current process by reading the process' stat file. + This should work for Linux. + """ + try: + stat = open('/proc/self/stat') + status = open('/proc/self/status') + except IOError: # pragma: no cover + return False + else: + stats = stat.read().split() + self.vsz = int(stats[22]) + self.rss = int(stats[23]) * self.pagesize + self.pagefaults = int(stats[11]) + + for entry in status.readlines(): + try: + key, value = entry.split(':', 1) + except ValueError: + continue + value = value.strip() + + def size_in_bytes(x: str) -> int: + return int(x.split()[0]) * 1024 + + if key == 'VmData': + self.data_segment = size_in_bytes(value) + elif key == 'VmExe': + self.code_segment = size_in_bytes(value) + elif key == 'VmLib': + self.shared_segment = size_in_bytes(value) + elif key == 'VmStk': + self.stack_segment = size_in_bytes(value) + key = self.key_map.get(key, '') + if key: + self.os_specific.append((key, pp(size_in_bytes(value)))) + + stat.close() + status.close() + return True + + +try: + from resource import getrusage, RUSAGE_SELF + + class _ProcessMemoryInfoResource(_ProcessMemoryInfo): + def update(self) -> bool: + """ + Get memory metrics of current process through `getrusage`. Only + available on Unix, on Linux most of the fields are not set, + and on BSD units are used that are not very helpful, see: + + http://www.perlmonks.org/?node_id=626693 + + Furthermore, getrusage only provides accumulated statistics (e.g. + max rss vs current rss). + """ + usage = getrusage(RUSAGE_SELF) + self.rss = usage.ru_maxrss * 1024 + self.data_segment = usage.ru_idrss * 1024 # TODO: ticks? + self.shared_segment = usage.ru_ixrss * 1024 # TODO: ticks? + self.stack_segment = usage.ru_isrss * 1024 # TODO: ticks? + self.vsz = (self.data_segment + self.shared_segment + + self.stack_segment) + + self.pagefaults = usage.ru_majflt + return self.rss != 0 + + if _ProcessMemoryInfoProc().update(): # pragma: no branch + ProcessMemoryInfo = _ProcessMemoryInfoProc + elif _ProcessMemoryInfoPS().update(): # pragma: no cover + ProcessMemoryInfo = _ProcessMemoryInfoPS + elif _ProcessMemoryInfoResource().update(): # pragma: no cover + ProcessMemoryInfo = _ProcessMemoryInfoResource + +except ImportError: + try: + # Requires pywin32 + from win32process import GetProcessMemoryInfo + from win32api import GetCurrentProcess, GlobalMemoryStatusEx + except ImportError: + logging.warn("Please install pywin32 when using pympler on Windows.") + else: + class _ProcessMemoryInfoWin32(_ProcessMemoryInfo): + def update(self) -> bool: + process_handle = GetCurrentProcess() + meminfo = GetProcessMemoryInfo(process_handle) + memstatus = GlobalMemoryStatusEx() + self.vsz = (memstatus['TotalVirtual'] - + memstatus['AvailVirtual']) + self.rss = meminfo['WorkingSetSize'] + self.pagefaults = meminfo['PageFaultCount'] + return True + + ProcessMemoryInfo = _ProcessMemoryInfoWin32 + + +class ThreadInfo(object): + """Collect information about an active thread.""" + + def __init__(self, thread: threading.Thread): + self.ident = thread.ident + self.name = thread.name + self.daemon = thread.daemon + + +def get_current_threads() -> Iterable[ThreadInfo]: + """Get a list of `ThreadInfo` objects.""" + return [ThreadInfo(thread) for thread in threading.enumerate()] + + +def get_current_thread_id() -> int: + """Get the ID of the current thread.""" + return threading.get_ident() diff --git a/venv/lib/python3.9/site-packages/pympler/refbrowser.py b/venv/lib/python3.9/site-packages/pympler/refbrowser.py new file mode 100644 index 00000000..ae8966fd --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/refbrowser.py @@ -0,0 +1,451 @@ +"""Tree-like exploration of object referrers. + +This module provides a base implementation for tree-like referrers browsing. +The two non-interactive classes ConsoleBrowser and FileBrowser output a tree +to the console or a file. One graphical user interface for referrers browsing +is provided as well. Further types can be subclassed. + +All types share a similar initialisation. That is, you provide a root object +and may specify further settings such as the initial depth of the tree or an +output function. +Afterwards you can print the tree which will be arranged based on your previous +settings. + +The interactive browser is based on a TreeWidget implemented in IDLE. It is +available only if you have Tcl/Tk installed. If you try to instantiate the +interactive browser without having Tkinter installed, an ImportError will be +raised. + +""" +import gc +import inspect +import sys + +from pympler import muppy +from pympler import summary + +from pympler.util.compat import tkinter + + +class _Node(object): + """A node as it is used in the tree structure. + + Each node contains the object it represents and a list of children. + Children can be other nodes or arbitrary other objects. Any object + in a tree which is not of the type _Node is considered a leaf. + + """ + def __init__(self, o, str_func=None): + """You have to define the object this node represents. Also you can + define an output function which will be used to represent this node. + If no function is defined, the default str representation is used. + + keyword arguments + str_func -- output function + + """ + self.o = o + self.children = [] + self.str_func = str_func + + def __str__(self): + """Override str(self.o) if str_func is defined.""" + if self.str_func is not None: + return self.str_func(self.o) + else: + return str(self.o) + + +class RefBrowser(object): + """Base class to other RefBrowser implementations. + + This base class provides means to extract a tree from a given root object + and holds information on already known objects (to avoid repetition + if requested). + + """ + + def __init__(self, rootobject, maxdepth=3, str_func=summary._repr, + repeat=True, stream=None): + """You have to provide the root object used in the refbrowser. + + keyword arguments + maxdepth -- maximum depth of the initial tree + str_func -- function used when calling str(node) + repeat -- should nodes appear repeatedly in the tree, or should be + referred to existing nodes + stream -- output stream (used in derived classes) + + """ + self.root = rootobject + self.maxdepth = maxdepth + self.str_func = str_func + self.repeat = repeat + self.stream = stream + # objects which should be ignored while building the tree + # e.g. the current frame + self.ignore = [] + # set of object ids which are already included + self.already_included = set() + self.ignore.append(self.already_included) + + def get_tree(self): + """Get a tree of referrers of the root object.""" + self.ignore.append(inspect.currentframe()) + return self._get_tree(self.root, self.maxdepth) + + def _get_tree(self, root, maxdepth): + """Workhorse of the get_tree implementation. + + This is a recursive method which is why we have a wrapper method. + root is the current root object of the tree which should be returned. + Note that root is not of the type _Node. + maxdepth defines how much further down the from the root the tree + should be build. + + """ + objects = gc.get_referrers(root) + res = _Node(root, self.str_func) + self.already_included.add(id(root)) + if maxdepth == 0: + return res + self.ignore.append(inspect.currentframe()) + self.ignore.append(objects) + for o in objects: + # Ignore dict of _Node and RefBrowser objects + if isinstance(o, dict): + if any(isinstance(ref, (_Node, RefBrowser)) + for ref in gc.get_referrers(o)): + continue + _id = id(o) + if not self.repeat and (_id in self.already_included): + s = self.str_func(o) + res.children.append("%s (already included, id %s)" % + (s, _id)) + continue + if (not isinstance(o, _Node)) and (o not in self.ignore): + res.children.append(self._get_tree(o, maxdepth - 1)) + return res + + +class StreamBrowser(RefBrowser): + """RefBrowser implementation which prints the tree to the console. + + If you don't like the looks, you can change it a little bit. + The class attributes 'hline', 'vline', 'cross', and 'space' can be + modified to your needs. + + """ + hline = '-' + vline = '|' + cross = '+' + space = ' ' + + def print_tree(self, tree=None): + """ Print referrers tree to console. + + keyword arguments + tree -- if not None, the passed tree will be printed. Otherwise it is + based on the rootobject. + + """ + if tree is None: + tree = self.get_tree() + self._print(tree, '', '') + + def _print(self, tree, prefix, carryon): + """Compute and print a new line of the tree. + + This is a recursive function. + + arguments + tree -- tree to print + prefix -- prefix to the current line to print + carryon -- prefix which is used to carry on the vertical lines + + """ + level = prefix.count(self.cross) + prefix.count(self.vline) + len_children = 0 + if isinstance(tree, _Node): + len_children = len(tree.children) + + # add vertex + prefix += str(tree) + # and as many spaces as the vertex is long + carryon += self.space * len(str(tree)) + if (level == self.maxdepth) or (not isinstance(tree, _Node)) or\ + (len_children == 0): + self.stream.write(prefix + '\n') + return + else: + # add in between connections + prefix += self.hline + carryon += self.space + # if there is more than one branch, add a cross + if len(tree.children) > 1: + prefix += self.cross + carryon += self.vline + prefix += self.hline + carryon += self.space + + if len_children > 0: + # print the first branch (on the same line) + self._print(tree.children[0], prefix, carryon) + for b in range(1, len_children): + # the carryon becomes the prefix for all following children + prefix = carryon[:-2] + self.cross + self.hline + # remove the vlines for any children of last branch + if b == (len_children - 1): + carryon = carryon[:-2] + 2 * self.space + self._print(tree.children[b], prefix, carryon) + # leave a free line before the next branch + if b == (len_children - 1): + if len(carryon.strip(' ')) == 0: + return + self.stream.write(carryon[:-2].rstrip() + '\n') + + +class ConsoleBrowser(StreamBrowser): + """RefBrowser that prints to the console (stdout).""" + + def __init__(self, *args, **kwargs): + super(ConsoleBrowser, self).__init__(*args, **kwargs) + if not self.stream: + self.stream = sys.stdout + + +class FileBrowser(StreamBrowser): + """RefBrowser implementation which prints the tree to a file.""" + + def print_tree(self, filename, tree=None): + """ Print referrers tree to file (in text format). + + keyword arguments + tree -- if not None, the passed tree will be printed. + + """ + old_stream = self.stream + self.stream = open(filename, 'w') + try: + super(FileBrowser, self).print_tree(tree=tree) + finally: + self.stream.close() + self.stream = old_stream + + +# Code for interactive browser (GUI) +# ================================== + +# The interactive browser requires Tkinter which is not always available. To +# avoid an import error when loading the module, we encapsulate most of the +# code in the following try-except-block. The InteractiveBrowser itself +# remains outside this block. If you try to instantiate it without having +# Tkinter installed, the import error will be raised. +try: + if sys.version_info < (3, 5, 2): + from idlelib import TreeWidget as _TreeWidget + else: + from idlelib import tree as _TreeWidget + + class _TreeNode(_TreeWidget.TreeNode): + """TreeNode used by the InteractiveBrowser. + + Not to be confused with _Node. This one is used in the GUI + context. + + """ + def reload_referrers(self): + """Reload all referrers for this _TreeNode.""" + self.item.node = self.item.reftree._get_tree(self.item.node.o, 1) + self.item._clear_children() + self.expand() + self.update() + + def print_object(self): + """Print object which this _TreeNode represents to console.""" + print(self.item.node.o) + + def drawtext(self): + """Override drawtext from _TreeWidget.TreeNode. + + This seems to be a good place to add the popup menu. + + """ + _TreeWidget.TreeNode.drawtext(self) + # create a menu + menu = tkinter.Menu(self.canvas, tearoff=0) + menu.add_command(label="reload referrers", + command=self.reload_referrers) + menu.add_command(label="print", command=self.print_object) + menu.add_separator() + menu.add_command(label="expand", command=self.expand) + menu.add_separator() + # the popup only disappears when to click on it + menu.add_command(label="Close Popup Menu") + + def do_popup(event): + menu.post(event.x_root, event.y_root) + + self.label.bind("<Button-3>", do_popup) + # override, i.e. disable the editing of items + + # disable editing of TreeNodes + def edit(self, event=None): + pass # see comment above + + def edit_finish(self, event=None): + pass # see comment above + + def edit_cancel(self, event=None): + pass # see comment above + + class _ReferrerTreeItem(_TreeWidget.TreeItem, tkinter.Label): + """Tree item wrapper around _Node object.""" + + def __init__(self, parentwindow, node, reftree): # constr calls + """You need to provide the parent window, the node this TreeItem + represents, as well as the tree (_Node) which the node + belongs to. + + """ + _TreeWidget.TreeItem.__init__(self) + tkinter.Label.__init__(self, parentwindow) + self.node = node + self.parentwindow = parentwindow + self.reftree = reftree + + def _clear_children(self): + """Clear children list from any TreeNode instances. + + Normally these objects are not required for memory profiling, as + they are part of the profiler. + + """ + new_children = [] + for child in self.node.children: + if not isinstance(child, _TreeNode): + new_children.append(child) + self.node.children = new_children + + def GetText(self): + return str(self.node) + + def GetIconName(self): + """Different icon when object cannot be expanded, i.e. has no + referrers. + + """ + if not self.IsExpandable(): + return "python" + + def IsExpandable(self): + """An object is expandable when it is a node which has children and + is a container object. + + """ + if not isinstance(self.node, _Node): + return False + else: + if len(self.node.children) > 0: + return True + else: + return muppy._is_containerobject(self.node.o) + + def GetSubList(self): + """This method is the point where further referrers are computed. + + Thus, the computation is done on-demand and only when needed. + + """ + sublist = [] + + children = self.node.children + if (len(children) == 0) and\ + (muppy._is_containerobject(self.node.o)): + self.node = self.reftree._get_tree(self.node.o, 1) + self._clear_children() + children = self.node.children + + for child in children: + item = _ReferrerTreeItem(self.parentwindow, child, + self.reftree) + sublist.append(item) + return sublist + +except ImportError: + _TreeWidget = None + + +def gui_default_str_function(o): + """Default str function for InteractiveBrowser.""" + return summary._repr(o) + '(id=%s)' % id(o) + + +class InteractiveBrowser(RefBrowser): + """Interactive referrers browser. + + The interactive browser is based on a TreeWidget implemented in IDLE. It is + available only if you have Tcl/Tk installed. If you try to instantiate the + interactive browser without having Tkinter installed, an ImportError will + be raised. + + """ + def __init__(self, rootobject, maxdepth=3, + str_func=gui_default_str_function, repeat=True): + """You have to provide the root object used in the refbrowser. + + keyword arguments + maxdepth -- maximum depth of the initial tree + str_func -- function used when calling str(node) + repeat -- should nodes appear repeatedly in the tree, or should be + referred to existing nodes + + """ + if tkinter is None: + raise ImportError( + "InteractiveBrowser requires Tkinter to be installed.") + RefBrowser.__init__(self, rootobject, maxdepth, str_func, repeat) + + def main(self, standalone=False): + """Create interactive browser window. + + keyword arguments + standalone -- Set to true, if the browser is not attached to other + windows + + """ + window = tkinter.Tk() + sc = _TreeWidget.ScrolledCanvas(window, bg="white", + highlightthickness=0, takefocus=1) + sc.frame.pack(expand=1, fill="both") + item = _ReferrerTreeItem(window, self.get_tree(), self) + node = _TreeNode(sc.canvas, None, item) + node.expand() + if standalone: + window.mainloop() + + +# list to hold to referrers +superlist = [] +root = "root" +for i in range(3): + tmp = [root] + superlist.append(tmp) + + +def foo(o): + return str(type(o)) + + +def print_sample(): + cb = ConsoleBrowser(root, str_func=foo) + cb.print_tree() + + +def write_sample(): + fb = FileBrowser(root, str_func=foo) + fb.print_tree('sample.txt') + + +if __name__ == "__main__": + write_sample() diff --git a/venv/lib/python3.9/site-packages/pympler/refgraph.py b/venv/lib/python3.9/site-packages/pympler/refgraph.py new file mode 100644 index 00000000..7f1f28bd --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/refgraph.py @@ -0,0 +1,350 @@ +""" +This module exposes utilities to illustrate objects and their references as +(directed) graphs. The current implementation requires 'graphviz' to be +installed. +""" + +from pympler.asizeof import Asizer, named_refs +from pympler.util.stringutils import safe_repr, trunc +from gc import get_referents +from subprocess import Popen, PIPE +from copy import copy +from sys import platform + +__all__ = ['ReferenceGraph'] + + +# Popen might lead to deadlocks when file descriptors are leaked to +# sub-processes on Linux. On Windows, however, close_fds=True leads to +# ValueError if stdin/stdout/stderr is piped: +# http://code.google.com/p/pympler/issues/detail?id=28#c1 +popen_flags = {} +if platform not in ['win32']: # pragma: no branch + popen_flags['close_fds'] = True + + +class _MetaObject(object): + """ + The _MetaObject stores meta-information, like a string representation, + corresponding to each object passed to a ReferenceGraph. + """ + __slots__ = ('size', 'id', 'type', 'str', 'group', 'cycle') + + def __init__(self): + self.cycle = False + + +class _Edge(object): + """ + Describes a reference from one object `src` to another object `dst`. + """ + __slots__ = ('src', 'dst', 'label', 'group') + + def __init__(self, src, dst, label): + self.src = src + self.dst = dst + self.label = label + self.group = None + + def __repr__(self): + return "<%08x => %08x, '%s', %s>" % (self.src, self.dst, self.label, + self.group) + + def __hash__(self): + return (self.src, self.dst, self.label).__hash__() + + def __eq__(self, other): + return self.__hash__() == other.__hash__() + + +class ReferenceGraph(object): + """ + The ReferenceGraph illustrates the references between a collection of + objects by rendering a directed graph. That requires that 'graphviz' is + installed. + + >>> from pympler.refgraph import ReferenceGraph + >>> a = 42 + >>> b = 'spam' + >>> c = {a: b} + >>> gb = ReferenceGraph([a,b,c]) + >>> gb.render('spam.eps') + True + """ + def __init__(self, objects, reduce=False): + """ + Initialize the ReferenceGraph with a collection of `objects`. + """ + self.objects = list(objects) + self.count = len(self.objects) + self.num_in_cycles = 'N/A' + self.edges = None + + if reduce: + self.num_in_cycles = self._reduce_to_cycles() + self._reduced = self # TODO: weakref? + else: + self._reduced = None + + self._get_edges() + self._annotate_objects() + + def _eliminate_leafs(self, graph): + """ + Eliminate leaf objects - that are objects not referencing any other + objects in the list `graph`. Returns the list of objects without the + objects identified as leafs. + """ + result = [] + idset = set([id(x) for x in graph]) + for n in graph: + refset = set([id(x) for x in get_referents(n)]) + if refset.intersection(idset): + result.append(n) + return result + + def _reduce_to_cycles(self): + """ + Iteratively eliminate leafs to reduce the set of objects to only those + that build cycles. Return the number of objects involved in reference + cycles. If there are no cycles, `self.objects` will be an empty list + and this method returns 0. + """ + cycles = self.objects[:] + cnt = 0 + while cnt != len(cycles): + cnt = len(cycles) + cycles = self._eliminate_leafs(cycles) + self.objects = cycles + return len(self.objects) + + def reduce_to_cycles(self): + """ + Iteratively eliminate leafs to reduce the set of objects to only those + that build cycles. Return the reduced graph. If there are no cycles, + None is returned. + """ + if not self._reduced: + reduced = copy(self) + reduced.objects = self.objects[:] + reduced.metadata = [] + reduced.edges = [] + self.num_in_cycles = reduced._reduce_to_cycles() + reduced.num_in_cycles = self.num_in_cycles + if self.num_in_cycles: + reduced._get_edges() + reduced._annotate_objects() + for meta in reduced.metadata: + meta.cycle = True + else: + reduced = None + self._reduced = reduced + return self._reduced + + def _get_edges(self): + """ + Compute the edges for the reference graph. + The function returns a set of tuples (id(a), id(b), ref) if a + references b with the referent 'ref'. + """ + idset = set([id(x) for x in self.objects]) + self.edges = set([]) + for n in self.objects: + refset = set([id(x) for x in get_referents(n)]) + for ref in refset.intersection(idset): + label = '' + members = None + if isinstance(n, dict): + members = n.items() + if not members: + members = named_refs(n) + for (k, v) in members: + if id(v) == ref: + label = k + break + self.edges.add(_Edge(id(n), ref, label)) + + def _annotate_groups(self): + """ + Annotate the objects belonging to separate (non-connected) graphs with + individual indices. + """ + g = {} + for x in self.metadata: + g[x.id] = x + + idx = 0 + for x in self.metadata: + if not hasattr(x, 'group'): + x.group = idx + idx += 1 + neighbors = set() + for e in self.edges: + if e.src == x.id: + neighbors.add(e.dst) + if e.dst == x.id: + neighbors.add(e.src) + for nb in neighbors: + g[nb].group = min(x.group, getattr(g[nb], 'group', idx)) + + # Assign the edges to the respective groups. Both "ends" of the edge + # should share the same group so just use the first object's group. + for e in self.edges: + e.group = g[e.src].group + + self._max_group = idx + + def _filter_group(self, group): + """ + Eliminate all objects but those which belong to `group`. + ``self.objects``, ``self.metadata`` and ``self.edges`` are modified. + Returns `True` if the group is non-empty. Otherwise returns `False`. + """ + self.metadata = [x for x in self.metadata if x.group == group] + group_set = set([x.id for x in self.metadata]) + self.objects = [obj for obj in self.objects if id(obj) in group_set] + self.count = len(self.metadata) + if self.metadata == []: + return False + + self.edges = [e for e in self.edges if e.group == group] + + del self._max_group + + return True + + def split(self): + """ + Split the graph into sub-graphs. Only connected objects belong to the + same graph. `split` yields copies of the Graph object. Shallow copies + are used that only replicate the meta-information, but share the same + object list ``self.objects``. + + >>> from pympler.refgraph import ReferenceGraph + >>> a = 42 + >>> b = 'spam' + >>> c = {a: b} + >>> t = (1,2,3) + >>> rg = ReferenceGraph([a,b,c,t]) + >>> for subgraph in rg.split(): + ... print (subgraph.index) + 0 + 1 + """ + self._annotate_groups() + index = 0 + + for group in range(self._max_group): + subgraph = copy(self) + subgraph.metadata = self.metadata[:] + subgraph.edges = self.edges.copy() + + if subgraph._filter_group(group): + subgraph.total_size = sum([x.size for x in subgraph.metadata]) + subgraph.index = index + index += 1 + yield subgraph + + def split_and_sort(self): + """ + Split the graphs into sub graphs and return a list of all graphs sorted + by the number of nodes. The graph with most nodes is returned first. + """ + graphs = list(self.split()) + graphs.sort(key=lambda x: -len(x.metadata)) + for index, graph in enumerate(graphs): + graph.index = index + return graphs + + def _annotate_objects(self): + """ + Extract meta-data describing the stored objects. + """ + self.metadata = [] + sizer = Asizer() + sizes = sizer.asizesof(*self.objects) + self.total_size = sizer.total + for obj, sz in zip(self.objects, sizes): + md = _MetaObject() + md.size = sz + md.id = id(obj) + try: + md.type = obj.__class__.__name__ + except (AttributeError, ReferenceError): # pragma: no cover + md.type = type(obj).__name__ + md.str = safe_repr(obj, clip=128) + self.metadata.append(md) + + def _get_graphviz_data(self): + """ + Emit a graph representing the connections between the objects described + within the metadata list. The text representation can be transformed to + a graph with graphviz. Returns a string. + """ + s = [] + header = '// Process this file with graphviz\n' + s.append(header) + s.append('digraph G {\n') + s.append(' node [shape=box];\n') + for md in self.metadata: + label = trunc(md.str, 48).replace('"', "'") + extra = '' + if md.type == 'instancemethod': + extra = ', color=red' + elif md.type == 'frame': + extra = ', color=orange' + s.append(' "X%s" [ label = "%s\\n%s" %s ];\n' % + (hex(md.id)[1:], label, md.type, extra)) + for e in self.edges: + extra = '' + if e.label == '__dict__': + extra = ',weight=100' + s.append(' X%s -> X%s [label="%s"%s];\n' % + (hex(e.src)[1:], hex(e.dst)[1:], e.label, extra)) + + s.append('}\n') + return "".join(s) + + def render(self, filename, cmd='dot', format='ps', unflatten=False): + """ + Render the graph to `filename` using graphviz. The graphviz invocation + command may be overridden by specifying `cmd`. The `format` may be any + specifier recognized by the graph renderer ('-Txxx' command). The + graph can be preprocessed by the *unflatten* tool if the `unflatten` + parameter is True. If there are no objects to illustrate, the method + does not invoke graphviz and returns False. If the renderer returns + successfully (return code 0), True is returned. + + An `OSError` is raised if the graphviz tool cannot be found. + """ + if self.objects == []: + return False + + data = self._get_graphviz_data() + + options = ('-Nfontsize=10', + '-Efontsize=10', + '-Nstyle=filled', + '-Nfillcolor=#E5EDB8', + '-Ncolor=#CCCCCC') + cmdline = (cmd, '-T%s' % format, '-o', filename) + options + + if unflatten: + p1 = Popen(('unflatten', '-l7'), stdin=PIPE, stdout=PIPE, + **popen_flags) + p2 = Popen(cmdline, stdin=p1.stdout, **popen_flags) + p1.communicate(data.encode()) + p2.communicate() + return p2.returncode == 0 + else: + p = Popen(cmdline, stdin=PIPE, **popen_flags) + p.communicate(data.encode()) + return p.returncode == 0 + + def write_graph(self, filename): + """ + Write raw graph data which can be post-processed using graphviz. + """ + f = open(filename, 'w') + f.write(self._get_graphviz_data()) + f.close() diff --git a/venv/lib/python3.9/site-packages/pympler/static/jquery.sparkline.min.js b/venv/lib/python3.9/site-packages/pympler/static/jquery.sparkline.min.js new file mode 100644 index 00000000..74187063 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/static/jquery.sparkline.min.js @@ -0,0 +1,5 @@ +/* jquery.sparkline 2.1.1 - http://omnipotent.net/jquery.sparkline/ +** Licensed under the New BSD License - see above site for details */ + +(function(a){typeof define=="function"&&define.amd?define(["jquery"],a):a(jQuery)})(function(a){"use strict";var b={},c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I=0;c=function(){return{common:{type:"line",lineColor:"#00f",fillColor:"#cdf",defaultPixelsPerValue:3,width:"auto",height:"auto",composite:!1,tagValuesAttribute:"values",tagOptionsPrefix:"spark",enableTagOptions:!1,enableHighlight:!0,highlightLighten:1.4,tooltipSkipNull:!0,tooltipPrefix:"",tooltipSuffix:"",disableHiddenCheck:!1,numberFormatter:!1,numberDigitGroupCount:3,numberDigitGroupSep:",",numberDecimalMark:".",disableTooltips:!1,disableInteraction:!1},line:{spotColor:"#f80",highlightSpotColor:"#5f5",highlightLineColor:"#f22",spotRadius:1.5,minSpotColor:"#f80",maxSpotColor:"#f80",lineWidth:1,normalRangeMin:undefined,normalRangeMax:undefined,normalRangeColor:"#ccc",drawNormalOnTop:!1,chartRangeMin:undefined,chartRangeMax:undefined,chartRangeMinX:undefined,chartRangeMaxX:undefined,tooltipFormat:new e('<span style="color: {{color}}">●</span> {{prefix}}{{y}}{{suffix}}')},bar:{barColor:"#3366cc",negBarColor:"#f44",stackedBarColor:["#3366cc","#dc3912","#ff9900","#109618","#66aa00","#dd4477","#0099c6","#990099"],zeroColor:undefined,nullColor:undefined,zeroAxis:!0,barWidth:4,barSpacing:1,chartRangeMax:undefined,chartRangeMin:undefined,chartRangeClip:!1,colorMap:undefined,tooltipFormat:new e('<span style="color: {{color}}">●</span> {{prefix}}{{value}}{{suffix}}')},tristate:{barWidth:4,barSpacing:1,posBarColor:"#6f6",negBarColor:"#f44",zeroBarColor:"#999",colorMap:{},tooltipFormat:new e('<span style="color: {{color}}">●</span> {{value:map}}'),tooltipValueLookups:{map:{"-1":"Loss",0:"Draw",1:"Win"}}},discrete:{lineHeight:"auto",thresholdColor:undefined,thresholdValue:0,chartRangeMax:undefined,chartRangeMin:undefined,chartRangeClip:!1,tooltipFormat:new e("{{prefix}}{{value}}{{suffix}}")},bullet:{targetColor:"#f33",targetWidth:3,performanceColor:"#33f",rangeColors:["#d3dafe","#a8b6ff","#7f94ff"],base:undefined,tooltipFormat:new e("{{fieldkey:fields}} - {{value}}"),tooltipValueLookups:{fields:{r:"Range",p:"Performance",t:"Target"}}},pie:{offset:0,sliceColors:["#3366cc","#dc3912","#ff9900","#109618","#66aa00","#dd4477","#0099c6","#990099"],borderWidth:0,borderColor:"#000",tooltipFormat:new e('<span style="color: {{color}}">●</span> {{value}} ({{percent.1}}%)')},box:{raw:!1,boxLineColor:"#000",boxFillColor:"#cdf",whiskerColor:"#000",outlierLineColor:"#333",outlierFillColor:"#fff",medianColor:"#f00",showOutliers:!0,outlierIQR:1.5,spotRadius:1.5,target:undefined,targetColor:"#4a2",chartRangeMax:undefined,chartRangeMin:undefined,tooltipFormat:new e("{{field:fields}}: {{value}}"),tooltipFormatFieldlistKey:"field",tooltipValueLookups:{fields:{lq:"Lower Quartile",med:"Median",uq:"Upper Quartile",lo:"Left Outlier",ro:"Right Outlier",lw:"Left Whisker",rw:"Right Whisker"}}}}},B='.jqstooltip { position: absolute;left: 0px;top: 0px;visibility: hidden;background: rgb(0, 0, 0) transparent;background-color: rgba(0,0,0,0.6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";color: white;font: 10px arial, san serif;text-align: left;white-space: nowrap;padding: 5px;border: 1px solid white;z-index: 10000;}.jqsfield { color: white;font: 10px arial, san serif;text-align: left;}',d=function(){var b,c;return b=function(){this.init.apply(this,arguments)},arguments.length>1?(arguments[0]?(b.prototype=a.extend(new arguments[0],arguments[arguments.length-1]),b._super=arguments[0].prototype):b.prototype=arguments[arguments.length-1],arguments.length>2&&(c=Array.prototype.slice.call(arguments,1,-1),c.unshift(b.prototype),a.extend.apply(a,c))):b.prototype=arguments[0],b.prototype.cls=b,b},a.SPFormatClass=e=d({fre:/\{\{([\w.]+?)(:(.+?))?\}\}/g,precre:/(\w+)\.(\d+)/,init:function(a,b){this.format=a,this.fclass=b},render:function(a,b,c){var d=this,e=a,f,g,h,i,j;return this.format.replace(this.fre,function(){var a;return g=arguments[1],h=arguments[3],f=d.precre.exec(g),f?(j=f[2],g=f[1]):j=!1,i=e[g],i===undefined?"":h&&b&&b[h]?(a=b[h],a.get?b[h].get(i)||i:b[h][i]||i):(k(i)&&(c.get("numberFormatter")?i=c.get("numberFormatter")(i):i=p(i,j,c.get("numberDigitGroupCount"),c.get("numberDigitGroupSep"),c.get("numberDecimalMark"))),i)})}}),a.spformat=function(a,b){return new e(a,b)},f=function(a,b,c){return a<b?b:a>c?c:a},g=function(a,b){var c;return b===2?(c=Math.floor(a.length/2),a.length%2?a[c]:(a[c-1]+a[c])/2):a.length%2?(c=(a.length*b+b)/4,c%1?(a[Math.floor(c)]+a[Math.floor(c)-1])/2:a[c-1]):(c=(a.length*b+2)/4,c%1?(a[Math.floor(c)]+a[Math.floor(c)-1])/2:a[c-1])},h=function(a){var b;switch(a){case"undefined":a=undefined;break;case"null":a=null;break;case"true":a=!0;break;case"false":a=!1;break;default:b=parseFloat(a),a==b&&(a=b)}return a},i=function(a){var b,c=[];for(b=a.length;b--;)c[b]=h(a[b]);return c},j=function(a,b){var c,d,e=[];for(c=0,d=a.length;c<d;c++)a[c]!==b&&e.push(a[c]);return e},k=function(a){return!isNaN(parseFloat(a))&&isFinite(a)},p=function(b,c,d,e,f){var g,h;b=(c===!1?parseFloat(b).toString():b.toFixed(c)).split(""),g=(g=a.inArray(".",b))<0?b.length:g,g<b.length&&(b[g]=f);for(h=g-d;h>0;h-=d)b.splice(h,0,e);return b.join("")},l=function(a,b,c){var d;for(d=b.length;d--;){if(c&&b[d]===null)continue;if(b[d]!==a)return!1}return!0},m=function(a){var b=0,c;for(c=a.length;c--;)b+=typeof a[c]=="number"?a[c]:0;return b},o=function(b){return a.isArray(b)?b:[b]},n=function(a){var b;document.createStyleSheet?document.createStyleSheet().cssText=a:(b=document.createElement("style"),b.type="text/css",document.getElementsByTagName("head")[0].appendChild(b),b[typeof document.body.style.WebkitAppearance=="string"?"innerText":"innerHTML"]=a)},a.fn.simpledraw=function(b,c,d,e){var f,g;if(d&&(f=this.data("_jqs_vcanvas")))return f;b===undefined&&(b=a(this).innerWidth()),c===undefined&&(c=a(this).innerHeight());if(a.fn.sparkline.hasCanvas)f=new F(b,c,this,e);else{if(!a.fn.sparkline.hasVML)return!1;f=new G(b,c,this)}return g=a(this).data("_jqs_mhandler"),g&&g.registerCanvas(f),f},a.fn.cleardraw=function(){var a=this.data("_jqs_vcanvas");a&&a.reset()},a.RangeMapClass=q=d({init:function(a){var b,c,d=[];for(b in a)a.hasOwnProperty(b)&&typeof b=="string"&&b.indexOf(":")>-1&&(c=b.split(":"),c[0]=c[0].length===0?-Infinity:parseFloat(c[0]),c[1]=c[1].length===0?Infinity:parseFloat(c[1]),c[2]=a[b],d.push(c));this.map=a,this.rangelist=d||!1},get:function(a){var b=this.rangelist,c,d,e;if((e=this.map[a])!==undefined)return e;if(b)for(c=b.length;c--;){d=b[c];if(d[0]<=a&&d[1]>=a)return d[2]}return undefined}}),a.range_map=function(a){return new q(a)},r=d({init:function(b,c){var d=a(b);this.$el=d,this.options=c,this.currentPageX=0,this.currentPageY=0,this.el=b,this.splist=[],this.tooltip=null,this.over=!1,this.displayTooltips=!c.get("disableTooltips"),this.highlightEnabled=!c.get("disableHighlight")},registerSparkline:function(a){this.splist.push(a),this.over&&this.updateDisplay()},registerCanvas:function(b){var c=a(b.canvas);this.canvas=b,this.$canvas=c,c.mouseenter(a.proxy(this.mouseenter,this)),c.mouseleave(a.proxy(this.mouseleave,this)),c.click(a.proxy(this.mouseclick,this))},reset:function(a){this.splist=[],this.tooltip&&a&&(this.tooltip.remove(),this.tooltip=undefined)},mouseclick:function(b){var c=a.Event("sparklineClick");c.originalEvent=b,c.sparklines=this.splist,this.$el.trigger(c)},mouseenter:function(b){a(document.body).unbind("mousemove.jqs"),a(document.body).bind("mousemove.jqs",a.proxy(this.mousemove,this)),this.over=!0,this.currentPageX=b.pageX,this.currentPageY=b.pageY,this.currentEl=b.target,!this.tooltip&&this.displayTooltips&&(this.tooltip=new s(this.options),this.tooltip.updatePosition(b.pageX,b.pageY)),this.updateDisplay()},mouseleave:function(){a(document.body).unbind("mousemove.jqs");var b=this.splist,c=b.length,d=!1,e,f;this.over=!1,this.currentEl=null,this.tooltip&&(this.tooltip.remove(),this.tooltip=null);for(f=0;f<c;f++)e=b[f],e.clearRegionHighlight()&&(d=!0);d&&this.canvas.render()},mousemove:function(a){this.currentPageX=a.pageX,this.currentPageY=a.pageY,this.currentEl=a.target,this.tooltip&&this.tooltip.updatePosition(a.pageX,a.pageY),this.updateDisplay()},updateDisplay:function(){var b=this.splist,c=b.length,d=!1,e=this.$canvas.offset(),f=this.currentPageX-e.left,g=this.currentPageY-e.top,h,i,j,k,l;if(!this.over)return;for(j=0;j<c;j++)i=b[j],k=i.setRegionHighlight(this.currentEl,f,g),k&&(d=!0);if(d){l=a.Event("sparklineRegionChange"),l.sparklines=this.splist,this.$el.trigger(l);if(this.tooltip){h="";for(j=0;j<c;j++)i=b[j],h+=i.getCurrentRegionTooltip();this.tooltip.setContent(h)}this.disableHighlight||this.canvas.render()}k===null&&this.mouseleave()}}),s=d({sizeStyle:"position: static !important;display: block !important;visibility: hidden !important;float: left !important;",init:function(b){var c=b.get("tooltipClassname","jqstooltip"),d=this.sizeStyle,e;this.container=b.get("tooltipContainer")||document.body,this.tooltipOffsetX=b.get("tooltipOffsetX",10),this.tooltipOffsetY=b.get("tooltipOffsetY",12),a("#jqssizetip").remove(),a("#jqstooltip").remove(),this.sizetip=a("<div/>",{id:"jqssizetip",style:d,"class":c}),this.tooltip=a("<div/>",{id:"jqstooltip","class":c}).appendTo(this.container),e=this.tooltip.offset(),this.offsetLeft=e.left,this.offsetTop=e.top,this.hidden=!0,a(window).unbind("resize.jqs scroll.jqs"),a(window).bind("resize.jqs scroll.jqs",a.proxy(this.updateWindowDims,this)),this.updateWindowDims()},updateWindowDims:function(){this.scrollTop=a(window).scrollTop(),this.scrollLeft=a(window).scrollLeft(),this.scrollRight=this.scrollLeft+a(window).width(),this.updatePosition()},getSize:function(a){this.sizetip.html(a).appendTo(this.container),this.width=this.sizetip.width()+1,this.height=this.sizetip.height(),this.sizetip.remove()},setContent:function(a){if(!a){this.tooltip.css("visibility","hidden"),this.hidden=!0;return}this.getSize(a),this.tooltip.html(a).css({width:this.width,height:this.height,visibility:"visible"}),this.hidden&&(this.hidden=!1,this.updatePosition())},updatePosition:function(a,b){if(a===undefined){if(this.mousex===undefined)return;a=this.mousex-this.offsetLeft,b=this.mousey-this.offsetTop}else this.mousex=a-=this.offsetLeft,this.mousey=b-=this.offsetTop;if(!this.height||!this.width||this.hidden)return;b-=this.height+this.tooltipOffsetY,a+=this.tooltipOffsetX,b<this.scrollTop&&(b=this.scrollTop),a<this.scrollLeft?a=this.scrollLeft:a+this.width>this.scrollRight&&(a=this.scrollRight-this.width),this.tooltip.css({left:a,top:b})},remove:function(){this.tooltip.remove(),this.sizetip.remove(),this.sizetip=this.tooltip=undefined,a(window).unbind("resize.jqs scroll.jqs")}}),C=function(){n(B)},a(C),H=[],a.fn.sparkline=function(b,c){return this.each(function(){var d=new a.fn.sparkline.options(this,c),e=a(this),f,g;f=function(){var c,f,g,h,i,j,k;if(b==="html"||b===undefined){k=this.getAttribute(d.get("tagValuesAttribute"));if(k===undefined||k===null)k=e.html();c=k.replace(/(^\s*<!--)|(-->\s*$)|\s+/g,"").split(",")}else c=b;f=d.get("width")==="auto"?c.length*d.get("defaultPixelsPerValue"):d.get("width");if(d.get("height")==="auto"){if(!d.get("composite")||!a.data(this,"_jqs_vcanvas"))h=document.createElement("span"),h.innerHTML="a",e.html(h),g=a(h).innerHeight()||a(h).height(),a(h).remove(),h=null}else g=d.get("height");d.get("disableInteraction")?i=!1:(i=a.data(this,"_jqs_mhandler"),i?d.get("composite")||i.reset():(i=new r(this,d),a.data(this,"_jqs_mhandler",i)));if(d.get("composite")&&!a.data(this,"_jqs_vcanvas")){a.data(this,"_jqs_errnotify")||(alert("Attempted to attach a composite sparkline to an element with no existing sparkline"),a.data(this,"_jqs_errnotify",!0));return}j=new(a.fn.sparkline[d.get("type")])(this,c,d,f,g),j.render(),i&&i.registerSparkline(j)};if(a(this).html()&&!d.get("disableHiddenCheck")&&a(this).is(":hidden")||a.fn.jquery<"1.3.0"&&a(this).parents().is(":hidden")||!a(this).parents("body").length){if(!d.get("composite")&&a.data(this,"_jqs_pending"))for(g=H.length;g;g--)H[g-1][0]==this&&H.splice(g-1,1);H.push([this,f]),a.data(this,"_jqs_pending",!0)}else f.call(this)})},a.fn.sparkline.defaults=c(),a.sparkline_display_visible=function(){var b,c,d,e=[];for(c=0,d=H.length;c<d;c++)b=H[c][0],a(b).is(":visible")&&!a(b).parents().is(":hidden")?(H[c][1].call(b),a.data(H[c][0],"_jqs_pending",!1),e.push(c)):!a(b).closest("html").length&&!a.data(b,"_jqs_pending")&&(a.data(H[c][0],"_jqs_pending",!1),e.push(c));for(c=e.length;c;c--)H.splice(e[c-1],1)},a.fn.sparkline.options=d({init:function(c,d){var e,f,g,h;this.userOptions=d=d||{},this.tag=c,this.tagValCache={},f=a.fn.sparkline.defaults,g=f.common,this.tagOptionsPrefix=d.enableTagOptions&&(d.tagOptionsPrefix||g.tagOptionsPrefix),h=this.getTagSetting("type"),h===b?e=f[d.type||g.type]:e=f[h],this.mergedOptions=a.extend({},g,e,d)},getTagSetting:function(a){var c=this.tagOptionsPrefix,d,e,f,g;if(c===!1||c===undefined)return b;if(this.tagValCache.hasOwnProperty(a))d=this.tagValCache.key;else{d=this.tag.getAttribute(c+a);if(d===undefined||d===null)d=b;else if(d.substr(0,1)==="["){d=d.substr(1,d.length-2).split(",");for(e=d.length;e--;)d[e]=h(d[e].replace(/(^\s*)|(\s*$)/g,""))}else if(d.substr(0,1)==="{"){f=d.substr(1,d.length-2).split(","),d={};for(e=f.length;e--;)g=f[e].split(":",2),d[g[0].replace(/(^\s*)|(\s*$)/g,"")]=h(g[1].replace(/(^\s*)|(\s*$)/g,""))}else d=h(d);this.tagValCache.key=d}return d},get:function(a,c){var d=this.getTagSetting(a),e;return d!==b?d:(e=this.mergedOptions[a])===undefined?c:e}}),a.fn.sparkline._base=d({disabled:!1,init:function(b,c,d,e,f){this.el=b,this.$el=a(b),this.values=c,this.options=d,this.width=e,this.height=f,this.currentRegion=undefined},initTarget:function(){var a=!this.options.get("disableInteraction");(this.target=this.$el.simpledraw(this.width,this.height,this.options.get("composite"),a))?(this.canvasWidth=this.target.pixelWidth,this.canvasHeight=this.target.pixelHeight):this.disabled=!0},render:function(){return this.disabled?(this.el.innerHTML="",!1):!0},getRegion:function(a,b){},setRegionHighlight:function(a,b,c){var d=this.currentRegion,e=!this.options.get("disableHighlight"),f;return b>this.canvasWidth||c>this.canvasHeight||b<0||c<0?null:(f=this.getRegion(a,b,c),d!==f?(d!==undefined&&e&&this.removeHighlight(),this.currentRegion=f,f!==undefined&&e&&this.renderHighlight(),!0):!1)},clearRegionHighlight:function(){return this.currentRegion!==undefined?(this.removeHighlight(),this.currentRegion=undefined,!0):!1},renderHighlight:function(){this.changeHighlight(!0)},removeHighlight:function(){this.changeHighlight(!1)},changeHighlight:function(a){},getCurrentRegionTooltip:function(){var b=this.options,c="",d=[],f,g,h,i,j,k,l,m,n,o,p,q,r,s;if(this.currentRegion===undefined)return"";f=this.getCurrentRegionFields(),p=b.get("tooltipFormatter");if(p)return p(this,b,f);b.get("tooltipChartTitle")&&(c+='<div class="jqs jqstitle">'+b.get("tooltipChartTitle")+"</div>\n"),g=this.options.get("tooltipFormat");if(!g)return"";a.isArray(g)||(g=[g]),a.isArray(f)||(f=[f]),l=this.options.get("tooltipFormatFieldlist"),m=this.options.get("tooltipFormatFieldlistKey");if(l&&m){n=[];for(k=f.length;k--;)o=f[k][m],(s=a.inArray(o,l))!=-1&&(n[s]=f[k]);f=n}h=g.length,r=f.length;for(k=0;k<h;k++){q=g[k],typeof q=="string"&&(q=new e(q)),i=q.fclass||"jqsfield";for(s=0;s<r;s++)if(!f[s].isNull||!b.get("tooltipSkipNull"))a.extend(f[s],{prefix:b.get("tooltipPrefix"),suffix:b.get("tooltipSuffix")}),j=q.render(f[s],b.get("tooltipValueLookups"),b),d.push('<div class="'+i+'">'+j+"</div>")}return d.length?c+d.join("\n"):""},getCurrentRegionFields:function(){},calcHighlightColor:function(a,b){var c=b.get("highlightColor"),d=b.get("highlightLighten"),e,g,h,i;if(c)return c;if(d){e=/^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(a)||/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(a);if(e){h=[],g=a.length===4?16:1;for(i=0;i<3;i++)h[i]=f(Math.round(parseInt(e[i+1],16)*g*d),0,255);return"rgb("+h.join(",")+")"}}return a}}),t={changeHighlight:function(b){var c=this.currentRegion,d=this.target,e=this.regionShapes[c],f;e&&(f=this.renderRegion(c,b),a.isArray(f)||a.isArray(e)?(d.replaceWithShapes(e,f),this.regionShapes[c]=a.map(f,function(a){return a.id})):(d.replaceWithShape(e,f),this.regionShapes[c]=f.id))},render:function(){var b=this.values,c=this.target,d=this.regionShapes,e,f,g,h;if(!this.cls._super.render.call(this))return;for(g=b.length;g--;){e=this.renderRegion(g);if(e)if(a.isArray(e)){f=[];for(h=e.length;h--;)e[h].append(),f.push(e[h].id);d[g]=f}else e.append(),d[g]=e.id;else d[g]=null}c.render()}},a.fn.sparkline.line=u=d(a.fn.sparkline._base,{type:"line",init:function(a,b,c,d,e){u._super.init.call(this,a,b,c,d,e),this.vertices=[],this.regionMap=[],this.xvalues=[],this.yvalues=[],this.yminmax=[],this.hightlightSpotId=null,this.lastShapeId=null,this.initTarget()},getRegion:function(a,b,c){var d,e=this.regionMap;for(d=e.length;d--;)if(e[d]!==null&&b>=e[d][0]&&b<=e[d][1])return e[d][2];return undefined},getCurrentRegionFields:function(){var a=this.currentRegion;return{isNull:this.yvalues[a]===null,x:this.xvalues[a],y:this.yvalues[a],color:this.options.get("lineColor"),fillColor:this.options.get("fillColor"),offset:a}},renderHighlight:function(){var a=this.currentRegion,b=this.target,c=this.vertices[a],d=this.options,e=d.get("spotRadius"),f=d.get("highlightSpotColor"),g=d.get("highlightLineColor"),h,i;if(!c)return;e&&f&&(h=b.drawCircle(c[0],c[1],e,undefined,f),this.highlightSpotId=h.id,b.insertAfterShape(this.lastShapeId,h)),g&&(i=b.drawLine(c[0],this.canvasTop,c[0],this.canvasTop+this.canvasHeight,g),this.highlightLineId=i.id,b.insertAfterShape(this.lastShapeId,i))},removeHighlight:function(){var a=this.target;this.highlightSpotId&&(a.removeShapeId(this.highlightSpotId),this.highlightSpotId=null),this.highlightLineId&&(a.removeShapeId(this.highlightLineId),this.highlightLineId=null)},scanValues:function(){var a=this.values,b=a.length,c=this.xvalues,d=this.yvalues,e=this.yminmax,f,g,h,i,j;for(f=0;f<b;f++)g=a[f],h=typeof a[f]=="string",i=typeof a[f]=="object"&&a[f]instanceof Array,j=h&&a[f].split(":"),h&&j.length===2?(c.push(Number(j[0])),d.push(Number(j[1])),e.push(Number(j[1]))):i?(c.push(g[0]),d.push(g[1]),e.push(g[1])):(c.push(f),a[f]===null||a[f]==="null"?d.push(null):(d.push(Number(g)),e.push(Number(g))));this.options.get("xvalues")&&(c=this.options.get("xvalues")),this.maxy=this.maxyorg=Math.max.apply(Math,e),this.miny=this.minyorg=Math.min.apply(Math,e),this.maxx=Math.max.apply(Math,c),this.minx=Math.min.apply(Math,c),this.xvalues=c,this.yvalues=d,this.yminmax=e},processRangeOptions:function(){var a=this.options,b=a.get("normalRangeMin"),c=a.get("normalRangeMax");b!==undefined&&(b<this.miny&&(this.miny=b),c>this.maxy&&(this.maxy=c)),a.get("chartRangeMin")!==undefined&&(a.get("chartRangeClip")||a.get("chartRangeMin")<this.miny)&&(this.miny=a.get("chartRangeMin")),a.get("chartRangeMax")!==undefined&&(a.get("chartRangeClip")||a.get("chartRangeMax")>this.maxy)&&(this.maxy=a.get("chartRangeMax")),a.get("chartRangeMinX")!==undefined&&(a.get("chartRangeClipX")||a.get("chartRangeMinX")<this.minx)&&(this.minx=a.get("chartRangeMinX")),a.get("chartRangeMaxX")!==undefined&&(a.get("chartRangeClipX")||a.get("chartRangeMaxX")>this.maxx)&&(this.maxx=a.get("chartRangeMaxX"))},drawNormalRange:function(a,b,c,d,e){var f=this.options.get("normalRangeMin"),g=this.options.get("normalRangeMax"),h=b+Math.round(c-c*((g-this.miny)/e)),i=Math.round(c*(g-f)/e);this.target.drawRect(a,h,d,i,undefined,this.options.get("normalRangeColor")).append()},render:function(){var b=this.options,c=this.target,d=this.canvasWidth,e=this.canvasHeight,f=this.vertices,g=b.get("spotRadius"),h=this.regionMap,i,j,k,l,m,n,o,p,r,s,t,v,w,x,y,z,A,B,C,D,E,F,G,H,I;if(!u._super.render.call(this))return;this.scanValues(),this.processRangeOptions(),G=this.xvalues,H=this.yvalues;if(!this.yminmax.length||this.yvalues.length<2)return;l=m=0,i=this.maxx-this.minx===0?1:this.maxx-this.minx,j=this.maxy-this.miny===0?1:this.maxy-this.miny,k=this.yvalues.length-1,g&&(d<g*4||e<g*4)&&(g=0);if(g){E=b.get("highlightSpotColor")&&!b.get("disableInteraction");if(E||b.get("minSpotColor")||b.get("spotColor")&&H[k]===this.miny)e-=Math.ceil(g);if(E||b.get("maxSpotColor")||b.get("spotColor")&&H[k]===this.maxy)e-=Math.ceil(g),l+=Math.ceil(g);if(E||(b.get("minSpotColor")||b.get("maxSpotColor"))&&(H[0]===this.miny||H[0]===this.maxy))m+=Math.ceil(g),d-=Math.ceil(g);if(E||b.get("spotColor")||b.get("minSpotColor")||b.get("maxSpotColor")&&(H[k]===this.miny||H[k]===this.maxy))d-=Math.ceil(g)}e--,b.get("normalRangeMin")!==undefined&&!b.get("drawNormalOnTop")&&this.drawNormalRange(m,l,e,d,j),o=[],p=[o],x=y=null,z=H.length;for(I=0;I<z;I++)r=G[I],t=G[I+1],s=H[I],v=m+Math.round((r-this.minx)*(d/i)),w=I<z-1?m+Math.round((t-this.minx)*(d/i)):d,y=v+(w-v)/2,h[I]=[x||0,y,I],x=y,s===null?I&&(H[I-1]!==null&&(o=[],p.push(o)),f.push(null)):(s<this.miny&&(s=this.miny),s>this.maxy&&(s=this.maxy),o.length||o.push([v,l+e]),n=[v,l+Math.round(e-e*((s-this.miny)/j))],o.push(n),f.push(n));A=[],B=[],C=p.length;for(I=0;I<C;I++)o=p[I],o.length&&(b.get("fillColor")&&(o.push([o[o.length-1][0],l+e]),B.push(o.slice(0)),o.pop()),o.length>2&&(o[0]=[o[0][0],o[1][1]]),A.push(o));C=B.length;for(I=0;I<C;I++)c.drawShape(B[I],b.get("fillColor"),b.get("fillColor")).append();b.get("normalRangeMin")!==undefined&&b.get("drawNormalOnTop")&&this.drawNormalRange(m,l,e,d,j),C=A.length;for(I=0;I<C;I++)c.drawShape(A[I],b.get("lineColor"),undefined,b.get("lineWidth")).append();if(g&&b.get("valueSpots")){D=b.get("valueSpots"),D.get===undefined&&(D=new q(D));for(I=0;I<z;I++)F=D.get(H[I]),F&&c.drawCircle(m+Math.round((G[I]-this.minx)*(d/i)),l+Math.round(e-e*((H[I]-this.miny)/j)),g,undefined,F).append()}g&&b.get("spotColor")&&H[k]!==null&&c.drawCircle(m+Math.round((G[G.length-1]-this.minx)*(d/i)),l+Math.round(e-e*((H[k]-this.miny)/j)),g,undefined,b.get("spotColor")).append(),this.maxy!==this.minyorg&&(g&&b.get("minSpotColor")&&(r=G[a.inArray(this.minyorg,H)],c.drawCircle(m+Math.round((r-this.minx)*(d/i)),l+Math.round(e-e*((this.minyorg-this.miny)/j)),g,undefined,b.get("minSpotColor")).append()),g&&b.get("maxSpotColor")&&(r=G[a.inArray(this.maxyorg,H)],c.drawCircle(m+Math.round((r-this.minx)*(d/i)),l+Math.round(e-e*((this.maxyorg-this.miny)/j)),g,undefined,b.get("maxSpotColor")).append())),this.lastShapeId=c.getLastShapeId(),this.canvasTop=l,c.render()}}),a.fn.sparkline.bar=v=d(a.fn.sparkline._base,t,{type:"bar",init:function(b,c,d,e,g){var k=parseInt(d.get("barWidth"),10),l=parseInt(d.get("barSpacing"),10),m=d.get("chartRangeMin"),n=d.get("chartRangeMax"),o=d.get("chartRangeClip"),p=Infinity,r=-Infinity,s,t,u,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P;v._super.init.call(this,b,c,d,e,g);for(y=0,z=c.length;y<z;y++){M=c[y],s=typeof M=="string"&&M.indexOf(":")>-1;if(s||a.isArray(M))H=!0,s&&(M=c[y]=i(M.split(":"))),M=j(M,null),t=Math.min.apply(Math,M),u=Math.max.apply(Math,M),t<p&&(p=t),u>r&&(r=u)}this.stacked=H,this.regionShapes={},this.barWidth=k,this.barSpacing=l,this.totalBarWidth=k+l,this.width=e=c.length*k+(c.length-1)*l,this.initTarget(),o&&(F=m===undefined?-Infinity:m,G=n===undefined?Infinity:n),x=[],w=H?[]:x;var Q=[],R=[];for(y=0,z=c.length;y<z;y++)if(H){I=c[y],c[y]=L=[],Q[y]=0,w[y]=R[y]=0;for(J=0,K=I.length;J<K;J++)M=L[J]=o?f(I[J],F,G):I[J],M!==null&&(M>0&&(Q[y]+=M),p<0&&r>0?M<0?R[y]+=Math.abs(M):w[y]+=M:w[y]+=Math.abs(M-(M<0?r:p)),x.push(M))}else M=o?f(c[y],F,G):c[y],M=c[y]=h(M),M!==null&&x.push(M);this.max=E=Math.max.apply(Math,x),this.min=D=Math.min.apply(Math,x),this.stackMax=r=H?Math.max.apply(Math,Q):E,this.stackMin=p=H?Math.min.apply(Math,x):D,d.get("chartRangeMin")!==undefined&&(d.get("chartRangeClip")||d.get("chartRangeMin")<D)&&(D=d.get("chartRangeMin")),d.get("chartRangeMax")!==undefined&&(d.get("chartRangeClip")||d.get("chartRangeMax")>E)&&(E=d.get("chartRangeMax")),this.zeroAxis=B=d.get("zeroAxis",!0),D<=0&&E>=0&&B?C=0:B==0?C=D:D>0?C=D:C=E,this.xaxisOffset=C,A=H?Math.max.apply(Math,w)+Math.max.apply(Math,R):E-D,this.canvasHeightEf=B&&D<0?this.canvasHeight-2:this.canvasHeight-1,D<C?(O=H&&E>=0?r:E,N=(O-C)/A*this.canvasHeight,N!==Math.ceil(N)&&(this.canvasHeightEf-=2,N=Math.ceil(N))):N=this.canvasHeight,this.yoffset=N,a.isArray(d.get("colorMap"))?(this.colorMapByIndex=d.get("colorMap"),this.colorMapByValue=null):(this.colorMapByIndex=null,this.colorMapByValue=d.get("colorMap"),this.colorMapByValue&&this.colorMapByValue.get===undefined&&(this.colorMapByValue=new q(this.colorMapByValue))),this.range=A},getRegion:function(a,b,c){var d=Math.floor(b/this.totalBarWidth);return d<0||d>=this.values.length?undefined:d},getCurrentRegionFields:function(){var a=this.currentRegion,b=o(this.values[a]),c=[],d,e;for(e=b.length;e--;)d=b[e],c.push({isNull:d===null,value:d,color:this.calcColor(e,d,a),offset:a});return c},calcColor:function(b,c,d){var e=this.colorMapByIndex,f=this.colorMapByValue,g=this.options,h,i;return this.stacked?h=g.get("stackedBarColor"):h=c<0?g.get("negBarColor"):g.get("barColor"),c===0&&g.get("zeroColor")!==undefined&&(h=g.get("zeroColor")),f&&(i=f.get(c))?h=i:e&&e.length>d&&(h=e[d]),a.isArray(h)?h[b%h.length]:h},renderRegion:function(b,c){var d=this.values[b],e=this.options,f=this.xaxisOffset,g=[],h=this.range,i=this.stacked,j=this.target,k=b*this.totalBarWidth,m=this.canvasHeightEf,n=this.yoffset,o,p,q,r,s,t,u,v,w,x;d=a.isArray(d)?d:[d],u=d.length,v=d[0],r=l(null,d),x=l(f,d,!0);if(r)return e.get("nullColor")?(q=c?e.get("nullColor"):this.calcHighlightColor(e.get("nullColor"),e),o=n>0?n-1:n,j.drawRect(k,o,this.barWidth-1,0,q,q)):undefined;s=n;for(t=0;t<u;t++){v=d[t];if(i&&v===f){if(!x||w)continue;w=!0}h>0?p=Math.floor(m*(Math.abs(v-f)/h))+1:p=1,v<f||v===f&&n===0?(o=s,s+=p):(o=n-p,n-=p),q=this.calcColor(t,v,b),c&&(q=this.calcHighlightColor(q,e)),g.push(j.drawRect(k,o,this.barWidth-1,p-1,q,q))}return g.length===1?g[0]:g}}),a.fn.sparkline.tristate=w=d(a.fn.sparkline._base,t,{type:"tristate",init:function(b,c,d,e,f){var g=parseInt(d.get("barWidth"),10),h=parseInt(d.get("barSpacing"),10);w._super.init.call(this,b,c,d,e,f),this.regionShapes={},this.barWidth=g,this.barSpacing=h,this.totalBarWidth=g+h,this.values=a.map(c,Number),this.width=e=c.length*g+(c.length-1)*h,a.isArray(d.get("colorMap"))?(this.colorMapByIndex=d.get("colorMap"),this.colorMapByValue=null):(this.colorMapByIndex=null,this.colorMapByValue=d.get("colorMap"),this.colorMapByValue&&this.colorMapByValue.get===undefined&&(this.colorMapByValue=new q(this.colorMapByValue))),this.initTarget()},getRegion:function(a,b,c){return Math.floor(b/this.totalBarWidth)},getCurrentRegionFields:function(){var a=this.currentRegion;return{isNull:this.values[a]===undefined,value:this.values[a],color:this.calcColor(this.values[a],a),offset:a}},calcColor:function(a,b){var c=this.values,d=this.options,e=this.colorMapByIndex,f=this.colorMapByValue,g,h;return f&&(h=f.get(a))?g=h:e&&e.length>b?g=e[b]:c[b]<0?g=d.get("negBarColor"):c[b]>0?g=d.get("posBarColor"):g=d.get("zeroBarColor"),g},renderRegion:function(a,b){var c=this.values,d=this.options,e=this.target,f,g,h,i,j,k;f=e.pixelHeight,h=Math.round(f/2),i=a*this.totalBarWidth,c[a]<0?(j=h,g=h-1):c[a]>0?(j=0,g=h-1):(j=h-1,g=2),k=this.calcColor(c[a],a);if(k===null)return;return b&&(k=this.calcHighlightColor(k,d)),e.drawRect(i,j,this.barWidth-1,g-1,k,k)}}),a.fn.sparkline.discrete=x=d(a.fn.sparkline._base,t,{type:"discrete",init:function(b,c,d,e,f){x._super.init.call(this,b,c,d,e,f),this.regionShapes={},this.values=c=a.map(c,Number),this.min=Math.min.apply(Math,c),this.max=Math.max.apply(Math,c),this.range=this.max-this.min,this.width=e=d.get("width")==="auto"?c.length*2:this.width,this.interval=Math.floor(e/c.length),this.itemWidth=e/c.length,d.get("chartRangeMin")!==undefined&&(d.get("chartRangeClip")||d.get("chartRangeMin")<this.min)&&(this.min=d.get("chartRangeMin")),d.get("chartRangeMax")!==undefined&&(d.get("chartRangeClip")||d.get("chartRangeMax")>this.max)&&(this.max=d.get("chartRangeMax")),this.initTarget(),this.target&&(this.lineHeight=d.get("lineHeight")==="auto"?Math.round(this.canvasHeight*.3):d.get("lineHeight"))},getRegion:function(a,b,c){return Math.floor(b/this.itemWidth)},getCurrentRegionFields:function(){var a=this.currentRegion;return{isNull:this.values[a]===undefined,value:this.values[a],offset:a}},renderRegion:function(a,b){var c=this.values,d=this.options,e=this.min,g=this.max,h=this.range,i=this.interval,j=this.target,k=this.canvasHeight,l=this.lineHeight,m=k-l,n,o,p,q;return o=f(c[a],e,g),q=a*i,n=Math.round(m-m*((o-e)/h)),p=d.get("thresholdColor")&&o<d.get("thresholdValue")?d.get("thresholdColor"):d.get("lineColor"),b&&(p=this.calcHighlightColor(p,d)),j.drawLine(q,n,q,n+l,p)}}),a.fn.sparkline.bullet=y=d(a.fn.sparkline._base,{type:"bullet",init:function(a,b,c,d,e){var f,g,h;y._super.init.call(this,a,b,c,d,e),this.values=b=i(b),h=b.slice(),h[0]=h[0]===null?h[2]:h[0],h[1]=b[1]===null?h[2]:h[1],f=Math.min.apply(Math,b),g=Math.max.apply(Math,b),c.get("base")===undefined?f=f<0?f:0:f=c.get("base"),this.min=f,this.max=g,this.range=g-f,this.shapes={},this.valueShapes={},this.regiondata={},this.width=d=c.get("width")==="auto"?"4.0em":d,this.target=this.$el.simpledraw(d,e,c.get("composite")),b.length||(this.disabled=!0),this.initTarget()},getRegion:function(a,b,c){var d=this.target.getShapeAt(a,b,c);return d!==undefined&&this.shapes[d]!==undefined?this.shapes[d]:undefined},getCurrentRegionFields:function(){var a=this.currentRegion;return{fieldkey:a.substr(0,1),value:this.values[a.substr(1)],region:a}},changeHighlight:function(a){var b=this.currentRegion,c=this.valueShapes[b],d;delete this.shapes[c];switch(b.substr(0,1)){case"r":d=this.renderRange(b.substr(1),a);break;case"p":d=this.renderPerformance(a);break;case"t":d=this.renderTarget(a)}this.valueShapes[b]=d.id,this.shapes[d.id]=b,this.target.replaceWithShape(c,d)},renderRange:function(a,b){var c=this.values[a],d=Math.round(this.canvasWidth*((c-this.min)/this.range)),e=this.options.get("rangeColors")[a-2];return b&&(e=this.calcHighlightColor(e,this.options)),this.target.drawRect(0,0,d-1,this.canvasHeight-1,e,e)},renderPerformance:function(a){var b=this.values[1],c=Math.round(this.canvasWidth*((b-this.min)/this.range)),d=this.options.get("performanceColor");return a&&(d=this.calcHighlightColor(d,this.options)),this.target.drawRect(0,Math.round(this.canvasHeight*.3),c-1,Math.round(this.canvasHeight*.4)-1,d,d)},renderTarget:function(a){var b=this.values[0],c=Math.round(this.canvasWidth*((b-this.min)/this.range)-this.options.get("targetWidth")/2),d=Math.round(this.canvasHeight*.1),e=this.canvasHeight-d*2,f=this.options.get("targetColor");return a&&(f=this.calcHighlightColor(f,this.options)),this.target.drawRect(c,d,this.options.get("targetWidth")-1,e-1,f,f)},render:function(){var a=this.values.length,b=this.target,c,d;if(!y._super.render.call(this))return;for(c=2;c<a;c++)d=this.renderRange(c).append(),this.shapes[d.id]="r"+c,this.valueShapes["r"+c]=d.id;this.values[1]!==null&&(d=this.renderPerformance().append(),this.shapes[d.id]="p1",this.valueShapes.p1=d.id),this.values[0]!==null&&(d=this.renderTarget().append(),this.shapes[d.id]="t0",this.valueShapes.t0=d.id),b.render()}}),a.fn.sparkline.pie=z=d(a.fn.sparkline._base,{type:"pie",init:function(b,c,d,e,f){var g=0,h;z._super.init.call(this,b,c,d,e,f),this.shapes={},this.valueShapes={},this.values=c=a.map(c,Number),d.get("width")==="auto"&&(this.width=this.height);if(c.length>0)for(h=c.length;h--;)g+=c[h];this.total=g,this.initTarget(),this.radius=Math.floor(Math.min(this.canvasWidth,this.canvasHeight)/2)},getRegion:function(a,b,c){var d=this.target.getShapeAt(a,b,c);return d!==undefined&&this.shapes[d]!==undefined?this.shapes[d]:undefined},getCurrentRegionFields:function(){var a=this.currentRegion;return{isNull:this.values[a]===undefined,value:this.values[a],percent:this.values[a]/this.total*100,color:this.options.get("sliceColors")[a%this.options.get("sliceColors").length],offset:a}},changeHighlight:function(a){var b=this.currentRegion,c=this.renderSlice(b,a),d=this.valueShapes[b];delete this.shapes[d],this.target.replaceWithShape(d,c),this.valueShapes[b]=c.id,this.shapes[c.id]=b},renderSlice:function(a,b){var c=this.target,d=this.options,e=this.radius,f=d.get("borderWidth"),g=d.get("offset"),h=2*Math.PI,i=this.values,j=this.total,k=g?2*Math.PI*(g/360):0,l,m,n,o,p;o=i.length;for(n=0;n<o;n++){l=k,m=k,j>0&&(m=k+h*(i[n]/j));if(a===n)return p=d.get("sliceColors")[n%d.get("sliceColors").length],b&&(p=this.calcHighlightColor(p,d)),c.drawPieSlice(e,e,e-f,l,m,undefined,p);k=m}},render:function(){var a=this.target,b=this.values,c=this.options,d=this.radius,e=c.get("borderWidth"),f,g;if(!z._super. +render.call(this))return;e&&a.drawCircle(d,d,Math.floor(d-e/2),c.get("borderColor"),undefined,e).append();for(g=b.length;g--;)b[g]&&(f=this.renderSlice(g).append(),this.valueShapes[g]=f.id,this.shapes[f.id]=g);a.render()}}),a.fn.sparkline.box=A=d(a.fn.sparkline._base,{type:"box",init:function(b,c,d,e,f){A._super.init.call(this,b,c,d,e,f),this.values=a.map(c,Number),this.width=d.get("width")==="auto"?"4.0em":e,this.initTarget(),this.values.length||(this.disabled=1)},getRegion:function(){return 1},getCurrentRegionFields:function(){var a=[{field:"lq",value:this.quartiles[0]},{field:"med",value:this.quartiles[1]},{field:"uq",value:this.quartiles[2]}];return this.loutlier!==undefined&&a.push({field:"lo",value:this.loutlier}),this.routlier!==undefined&&a.push({field:"ro",value:this.routlier}),this.lwhisker!==undefined&&a.push({field:"lw",value:this.lwhisker}),this.rwhisker!==undefined&&a.push({field:"rw",value:this.rwhisker}),a},render:function(){var a=this.target,b=this.values,c=b.length,d=this.options,e=this.canvasWidth,f=this.canvasHeight,h=d.get("chartRangeMin")===undefined?Math.min.apply(Math,b):d.get("chartRangeMin"),i=d.get("chartRangeMax")===undefined?Math.max.apply(Math,b):d.get("chartRangeMax"),j=0,k,l,m,n,o,p,q,r,s,t,u;if(!A._super.render.call(this))return;if(d.get("raw"))d.get("showOutliers")&&b.length>5?(l=b[0],k=b[1],n=b[2],o=b[3],p=b[4],q=b[5],r=b[6]):(k=b[0],n=b[1],o=b[2],p=b[3],q=b[4]);else{b.sort(function(a,b){return a-b}),n=g(b,1),o=g(b,2),p=g(b,3),m=p-n;if(d.get("showOutliers")){k=q=undefined;for(s=0;s<c;s++)k===undefined&&b[s]>n-m*d.get("outlierIQR")&&(k=b[s]),b[s]<p+m*d.get("outlierIQR")&&(q=b[s]);l=b[0],r=b[c-1]}else k=b[0],q=b[c-1]}this.quartiles=[n,o,p],this.lwhisker=k,this.rwhisker=q,this.loutlier=l,this.routlier=r,u=e/(i-h+1),d.get("showOutliers")&&(j=Math.ceil(d.get("spotRadius")),e-=2*Math.ceil(d.get("spotRadius")),u=e/(i-h+1),l<k&&a.drawCircle((l-h)*u+j,f/2,d.get("spotRadius"),d.get("outlierLineColor"),d.get("outlierFillColor")).append(),r>q&&a.drawCircle((r-h)*u+j,f/2,d.get("spotRadius"),d.get("outlierLineColor"),d.get("outlierFillColor")).append()),a.drawRect(Math.round((n-h)*u+j),Math.round(f*.1),Math.round((p-n)*u),Math.round(f*.8),d.get("boxLineColor"),d.get("boxFillColor")).append(),a.drawLine(Math.round((k-h)*u+j),Math.round(f/2),Math.round((n-h)*u+j),Math.round(f/2),d.get("lineColor")).append(),a.drawLine(Math.round((k-h)*u+j),Math.round(f/4),Math.round((k-h)*u+j),Math.round(f-f/4),d.get("whiskerColor")).append(),a.drawLine(Math.round((q-h)*u+j),Math.round(f/2),Math.round((p-h)*u+j),Math.round(f/2),d.get("lineColor")).append(),a.drawLine(Math.round((q-h)*u+j),Math.round(f/4),Math.round((q-h)*u+j),Math.round(f-f/4),d.get("whiskerColor")).append(),a.drawLine(Math.round((o-h)*u+j),Math.round(f*.1),Math.round((o-h)*u+j),Math.round(f*.9),d.get("medianColor")).append(),d.get("target")&&(t=Math.ceil(d.get("spotRadius")),a.drawLine(Math.round((d.get("target")-h)*u+j),Math.round(f/2-t),Math.round((d.get("target")-h)*u+j),Math.round(f/2+t),d.get("targetColor")).append(),a.drawLine(Math.round((d.get("target")-h)*u+j-t),Math.round(f/2),Math.round((d.get("target")-h)*u+j+t),Math.round(f/2),d.get("targetColor")).append()),a.render()}}),function(){document.namespaces&&!document.namespaces.v?(a.fn.sparkline.hasVML=!0,document.namespaces.add("v","urn:schemas-microsoft-com:vml","#default#VML")):a.fn.sparkline.hasVML=!1;var b=document.createElement("canvas");a.fn.sparkline.hasCanvas=!!b.getContext&&!!b.getContext("2d")}(),D=d({init:function(a,b,c,d){this.target=a,this.id=b,this.type=c,this.args=d},append:function(){return this.target.appendShape(this),this}}),E=d({_pxregex:/(\d+)(px)?\s*$/i,init:function(b,c,d){if(!b)return;this.width=b,this.height=c,this.target=d,this.lastShapeId=null,d[0]&&(d=d[0]),a.data(d,"_jqs_vcanvas",this)},drawLine:function(a,b,c,d,e,f){return this.drawShape([[a,b],[c,d]],e,f)},drawShape:function(a,b,c,d){return this._genShape("Shape",[a,b,c,d])},drawCircle:function(a,b,c,d,e,f){return this._genShape("Circle",[a,b,c,d,e,f])},drawPieSlice:function(a,b,c,d,e,f,g){return this._genShape("PieSlice",[a,b,c,d,e,f,g])},drawRect:function(a,b,c,d,e,f){return this._genShape("Rect",[a,b,c,d,e,f])},getElement:function(){return this.canvas},getLastShapeId:function(){return this.lastShapeId},reset:function(){alert("reset not implemented")},_insert:function(b,c){a(c).html(b)},_calculatePixelDims:function(b,c,d){var e;e=this._pxregex.exec(c),e?this.pixelHeight=e[1]:this.pixelHeight=a(d).height(),e=this._pxregex.exec(b),e?this.pixelWidth=e[1]:this.pixelWidth=a(d).width()},_genShape:function(a,b){var c=I++;return b.unshift(c),new D(this,c,a,b)},appendShape:function(a){alert("appendShape not implemented")},replaceWithShape:function(a,b){alert("replaceWithShape not implemented")},insertAfterShape:function(a,b){alert("insertAfterShape not implemented")},removeShapeId:function(a){alert("removeShapeId not implemented")},getShapeAt:function(a,b,c){alert("getShapeAt not implemented")},render:function(){alert("render not implemented")}}),F=d(E,{init:function(b,c,d,e){F._super.init.call(this,b,c,d),this.canvas=document.createElement("canvas"),d[0]&&(d=d[0]),a.data(d,"_jqs_vcanvas",this),a(this.canvas).css({display:"inline-block",width:b,height:c,verticalAlign:"top"}),this._insert(this.canvas,d),this._calculatePixelDims(b,c,this.canvas),this.canvas.width=this.pixelWidth,this.canvas.height=this.pixelHeight,this.interact=e,this.shapes={},this.shapeseq=[],this.currentTargetShapeId=undefined,a(this.canvas).css({width:this.pixelWidth,height:this.pixelHeight})},_getContext:function(a,b,c){var d=this.canvas.getContext("2d");return a!==undefined&&(d.strokeStyle=a),d.lineWidth=c===undefined?1:c,b!==undefined&&(d.fillStyle=b),d},reset:function(){var a=this._getContext();a.clearRect(0,0,this.pixelWidth,this.pixelHeight),this.shapes={},this.shapeseq=[],this.currentTargetShapeId=undefined},_drawShape:function(a,b,c,d,e){var f=this._getContext(c,d,e),g,h;f.beginPath(),f.moveTo(b[0][0]+.5,b[0][1]+.5);for(g=1,h=b.length;g<h;g++)f.lineTo(b[g][0]+.5,b[g][1]+.5);c!==undefined&&f.stroke(),d!==undefined&&f.fill(),this.targetX!==undefined&&this.targetY!==undefined&&f.isPointInPath(this.targetX,this.targetY)&&(this.currentTargetShapeId=a)},_drawCircle:function(a,b,c,d,e,f,g){var h=this._getContext(e,f,g);h.beginPath(),h.arc(b,c,d,0,2*Math.PI,!1),this.targetX!==undefined&&this.targetY!==undefined&&h.isPointInPath(this.targetX,this.targetY)&&(this.currentTargetShapeId=a),e!==undefined&&h.stroke(),f!==undefined&&h.fill()},_drawPieSlice:function(a,b,c,d,e,f,g,h){var i=this._getContext(g,h);i.beginPath(),i.moveTo(b,c),i.arc(b,c,d,e,f,!1),i.lineTo(b,c),i.closePath(),g!==undefined&&i.stroke(),h&&i.fill(),this.targetX!==undefined&&this.targetY!==undefined&&i.isPointInPath(this.targetX,this.targetY)&&(this.currentTargetShapeId=a)},_drawRect:function(a,b,c,d,e,f,g){return this._drawShape(a,[[b,c],[b+d,c],[b+d,c+e],[b,c+e],[b,c]],f,g)},appendShape:function(a){return this.shapes[a.id]=a,this.shapeseq.push(a.id),this.lastShapeId=a.id,a.id},replaceWithShape:function(a,b){var c=this.shapeseq,d;this.shapes[b.id]=b;for(d=c.length;d--;)c[d]==a&&(c[d]=b.id);delete this.shapes[a]},replaceWithShapes:function(a,b){var c=this.shapeseq,d={},e,f,g;for(f=a.length;f--;)d[a[f]]=!0;for(f=c.length;f--;)e=c[f],d[e]&&(c.splice(f,1),delete this.shapes[e],g=f);for(f=b.length;f--;)c.splice(g,0,b[f].id),this.shapes[b[f].id]=b[f]},insertAfterShape:function(a,b){var c=this.shapeseq,d;for(d=c.length;d--;)if(c[d]===a){c.splice(d+1,0,b.id),this.shapes[b.id]=b;return}},removeShapeId:function(a){var b=this.shapeseq,c;for(c=b.length;c--;)if(b[c]===a){b.splice(c,1);break}delete this.shapes[a]},getShapeAt:function(a,b,c){return this.targetX=b,this.targetY=c,this.render(),this.currentTargetShapeId},render:function(){var a=this.shapeseq,b=this.shapes,c=a.length,d=this._getContext(),e,f,g;d.clearRect(0,0,this.pixelWidth,this.pixelHeight);for(g=0;g<c;g++)e=a[g],f=b[e],this["_draw"+f.type].apply(this,f.args);this.interact||(this.shapes={},this.shapeseq=[])}}),G=d(E,{init:function(b,c,d){var e;G._super.init.call(this,b,c,d),d[0]&&(d=d[0]),a.data(d,"_jqs_vcanvas",this),this.canvas=document.createElement("span"),a(this.canvas).css({display:"inline-block",position:"relative",overflow:"hidden",width:b,height:c,margin:"0px",padding:"0px",verticalAlign:"top"}),this._insert(this.canvas,d),this._calculatePixelDims(b,c,this.canvas),this.canvas.width=this.pixelWidth,this.canvas.height=this.pixelHeight,e='<v:group coordorigin="0 0" coordsize="'+this.pixelWidth+" "+this.pixelHeight+'"'+' style="position:absolute;top:0;left:0;width:'+this.pixelWidth+"px;height="+this.pixelHeight+'px;"></v:group>',this.canvas.insertAdjacentHTML("beforeEnd",e),this.group=a(this.canvas).children()[0],this.rendered=!1,this.prerender=""},_drawShape:function(a,b,c,d,e){var f=[],g,h,i,j,k,l,m;for(m=0,l=b.length;m<l;m++)f[m]=""+b[m][0]+","+b[m][1];return g=f.splice(0,1),e=e===undefined?1:e,h=c===undefined?' stroked="false" ':' strokeWeight="'+e+'px" strokeColor="'+c+'" ',i=d===undefined?' filled="false"':' fillColor="'+d+'" filled="true" ',j=f[0]===f[f.length-1]?"x ":"",k='<v:shape coordorigin="0 0" coordsize="'+this.pixelWidth+" "+this.pixelHeight+'" '+' id="jqsshape'+a+'" '+h+i+' style="position:absolute;left:0px;top:0px;height:'+this.pixelHeight+"px;width:"+this.pixelWidth+'px;padding:0px;margin:0px;" '+' path="m '+g+" l "+f.join(", ")+" "+j+'e">'+" </v:shape>",k},_drawCircle:function(a,b,c,d,e,f,g){var h,i,j;return b-=d,c-=d,h=e===undefined?' stroked="false" ':' strokeWeight="'+g+'px" strokeColor="'+e+'" ',i=f===undefined?' filled="false"':' fillColor="'+f+'" filled="true" ',j='<v:oval id="jqsshape'+a+'" '+h+i+' style="position:absolute;top:'+c+"px; left:"+b+"px; width:"+d*2+"px; height:"+d*2+'px"></v:oval>',j},_drawPieSlice:function(a,b,c,d,e,f,g,h){var i,j,k,l,m,n,o,p;if(e===f)return"";f-e===2*Math.PI&&(e=0,f=2*Math.PI),j=b+Math.round(Math.cos(e)*d),k=c+Math.round(Math.sin(e)*d),l=b+Math.round(Math.cos(f)*d),m=c+Math.round(Math.sin(f)*d);if(j===l&&k===m){if(f-e<Math.PI)return"";j=l=b+d,k=m=c}return j===l&&k===m&&f-e<Math.PI?"":(i=[b-d,c-d,b+d,c+d,j,k,l,m],n=g===undefined?' stroked="false" ':' strokeWeight="1px" strokeColor="'+g+'" ',o=h===undefined?' filled="false"':' fillColor="'+h+'" filled="true" ',p='<v:shape coordorigin="0 0" coordsize="'+this.pixelWidth+" "+this.pixelHeight+'" '+' id="jqsshape'+a+'" '+n+o+' style="position:absolute;left:0px;top:0px;height:'+this.pixelHeight+"px;width:"+this.pixelWidth+'px;padding:0px;margin:0px;" '+' path="m '+b+","+c+" wa "+i.join(", ")+' x e">'+" </v:shape>",p)},_drawRect:function(a,b,c,d,e,f,g){return this._drawShape(a,[[b,c],[b,c+e],[b+d,c+e],[b+d,c],[b,c]],f,g)},reset:function(){this.group.innerHTML=""},appendShape:function(a){var b=this["_draw"+a.type].apply(this,a.args);return this.rendered?this.group.insertAdjacentHTML("beforeEnd",b):this.prerender+=b,this.lastShapeId=a.id,a.id},replaceWithShape:function(b,c){var d=a("#jqsshape"+b),e=this["_draw"+c.type].apply(this,c.args);d[0].outerHTML=e},replaceWithShapes:function(b,c){var d=a("#jqsshape"+b[0]),e="",f=c.length,g;for(g=0;g<f;g++)e+=this["_draw"+c[g].type].apply(this,c[g].args);d[0].outerHTML=e;for(g=1;g<b.length;g++)a("#jqsshape"+b[g]).remove()},insertAfterShape:function(b,c){var d=a("#jqsshape"+b),e=this["_draw"+c.type].apply(this,c.args);d[0].insertAdjacentHTML("afterEnd",e)},removeShapeId:function(b){var c=a("#jqsshape"+b);this.group.removeChild(c[0])},getShapeAt:function(a,b,c){var d=a.id.substr(8);return d},render:function(){this.rendered||(this.group.innerHTML=this.prerender,this.rendered=!0)}})});
\ No newline at end of file diff --git a/venv/lib/python3.9/site-packages/pympler/summary.py b/venv/lib/python3.9/site-packages/pympler/summary.py new file mode 100644 index 00000000..7c9470fd --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/summary.py @@ -0,0 +1,321 @@ +"""A collection of functions to summarize object information. + +This module provides several function which will help you to analyze object +information which was gathered. Often it is sufficient to work with aggregated +data instead of handling the entire set of existing objects. For example can a +memory leak identified simple based on the number and size of existing objects. + +A summary contains information about objects in a table-like manner. +Technically, it is a list of lists. Each of these lists represents a row, +whereas the first column reflects the object type, the second column the number +of objects, and the third column the size of all these objects. This allows a +simple table-like output like the following: + +============= ============ ============= + types # objects total size +============= ============ ============= +<type 'dict'> 2 560 + <type 'str'> 3 126 + <type 'int'> 4 96 +<type 'long'> 2 66 +<type 'list'> 1 40 +============= ============ ============= + +Another advantage of summaries is that they influence the system you analyze +only to a minimum. Working with references to existing objects will keep these +objects alive. Most of the times this is no desired behavior (as it will have +an impact on the observations). Using summaries reduces this effect greatly. + +output representation +--------------------- + +The output representation of types is defined in summary.representations. +Every type defined in this dictionary will be represented as specified. Each +definition has a list of different representations. The later a representation +appears in this list, the higher its verbosity level. From types which are not +defined in summary.representations the default str() representation will be +used. + +Per default, summaries will use the verbosity level 1 for any encountered type. +The reason is that several computations are done with summaries and rows have +to remain comparable. Therefore information which reflect an objects state, +e.g. the current line number of a frame, should not be included. You may add +more detailed information at higher verbosity levels than 1. +""" + +import re +import sys +import types + +from pympler.util import stringutils +from sys import getsizeof + +representations = {} + + +def _init_representations(): + global representations + if sys.hexversion < 0x2040000: + classobj = [ + lambda c: "classobj(%s)" % repr(c), + ] + representations[types.ClassType] = classobj + instance = [ + lambda f: "instance(%s)" % repr(f.__class__), + ] + representations[types.InstanceType] = instance + instancemethod = [ + lambda i: "instancemethod (%s)" % (repr(i.im_func)), + lambda i: "instancemethod (%s, %s)" % (repr(i.im_class), + repr(i.im_func)), + ] + representations[types.MethodType] = instancemethod + frame = [ + lambda f: "frame (codename: %s)" % (f.f_code.co_name), + lambda f: "frame (codename: %s, codeline: %s)" % + (f.f_code.co_name, f.f_code.co_firstlineno), + lambda f: "frame (codename: %s, filename: %s, codeline: %s)" % + (f.f_code.co_name, f.f_code.co_filename, + f.f_code.co_firstlineno) + ] + representations[types.FrameType] = frame + _dict = [ + lambda d: str(type(d)), + lambda d: "dict, len=%s" % len(d), + ] + representations[dict] = _dict + function = [ + lambda f: "function (%s)" % f.__name__, + lambda f: "function (%s.%s)" % (f.__module__, f.__name__), + ] + representations[types.FunctionType] = function + _list = [ + lambda l: str(type(l)), + lambda l: "list, len=%s" % len(l) + ] + representations[list] = _list + module = [lambda m: "module(%s)" % getattr( + m, '__name__', getattr(m, '__file__', 'nameless, id: %d' % id(m)) + )] + representations[types.ModuleType] = module + _set = [ + lambda s: str(type(s)), + lambda s: "set, len=%s" % len(s) + ] + representations[set] = _set + + +_init_representations() + + +def summarize(objects): + """Summarize an objects list. + + Return a list of lists, whereas each row consists of:: + [str(type), number of objects of this type, total size of these objects]. + + No guarantee regarding the order is given. + + """ + count = {} + total_size = {} + for o in objects: + otype = _repr(o) + if otype in count: + count[otype] += 1 + total_size[otype] += getsizeof(o) + else: + count[otype] = 1 + total_size[otype] = getsizeof(o) + rows = [] + for otype in count: + rows.append([otype, count[otype], total_size[otype]]) + return rows + + +def get_diff(left, right): + """Get the difference of two summaries. + + Subtracts the values of the right summary from the values of the left + summary. + If similar rows appear on both sides, the are included in the summary with + 0 for number of elements and total size. + If the number of elements of a row of the diff is 0, but the total size is + not, it means that objects likely have changed, but not there number, thus + resulting in a changed size. + + """ + res = [] + + right_by_key = dict((r[0], r) for r in right) + left_by_key = dict((r[0], r) for r in left) + + keys = set(right_by_key) + keys.update(left_by_key) + + for key in keys: + r = right_by_key.get(key) + l = left_by_key.get(key) + if l and r: + res.append([key, r[1] - l[1], r[2] - l[2]]) + elif r: + res.append(r) + elif l: + res.append([key, -l[1], -l[2]]) + else: + continue # shouldn't happen + return res + + +def format_(rows, limit=15, sort='size', order='descending'): + """Format the rows as a summary. + + Keyword arguments: + limit -- the maximum number of elements to be listed + sort -- sort elements by 'size', 'type', or '#' + order -- sort 'ascending' or 'descending' + """ + localrows = [] + for row in rows: + localrows.append(list(row)) + # input validation + sortby = ['type', '#', 'size'] + if sort not in sortby: + raise ValueError("invalid sort, should be one of" + str(sortby)) + orders = ['ascending', 'descending'] + if order not in orders: + raise ValueError("invalid order, should be one of" + str(orders)) + # sort rows + if sortby.index(sort) == 0: + if order == "ascending": + localrows.sort(key=lambda x: _repr(x[0])) + elif order == "descending": + localrows.sort(key=lambda x: _repr(x[0]), reverse=True) + else: + if order == "ascending": + localrows.sort(key=lambda x: x[sortby.index(sort)]) + elif order == "descending": + localrows.sort(key=lambda x: x[sortby.index(sort)], reverse=True) + # limit rows + localrows = localrows[0:limit] + for row in localrows: + row[2] = stringutils.pp(row[2]) + # print rows + localrows.insert(0, ["types", "# objects", "total size"]) + return _format_table(localrows) + + +def _format_table(rows, header=True): + """Format a list of lists as a pretty table. + + Keyword arguments: + header -- if True the first row is treated as a table header + + inspired by http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/267662 + """ + border = "=" + # vertical delimiter + vdelim = " | " + # padding nr. of spaces are left around the longest element in the + # column + padding = 1 + # may be left,center,right + justify = 'right' + justify = {'left': str.ljust, + 'center': str.center, + 'right': str.rjust}[justify.lower()] + # calculate column widths (longest item in each col + # plus "padding" nr of spaces on both sides) + cols = zip(*rows) + colWidths = [max([len(str(item)) + 2 * padding for item in col]) + for col in cols] + borderline = vdelim.join([w * border for w in colWidths]) + for row in rows: + yield vdelim.join([justify(str(item), width) + for (item, width) in zip(row, colWidths)]) + if header: + yield borderline + header = False + + +def print_(rows, limit=15, sort='size', order='descending'): + """Print the rows as a summary. + + Keyword arguments: + limit -- the maximum number of elements to be listed + sort -- sort elements by 'size', 'type', or '#' + order -- sort 'ascending' or 'descending' + + """ + for line in format_(rows, limit=limit, sort=sort, order=order): + print(line) + + +# regular expressions used by _repr to replace default type representations +type_repr = re.compile(r"^<(type|class) '(\S+)'>$") +address = re.compile(r' at 0x[0-9a-f]+') + + +def _repr(o, verbosity=1): + """Get meaning object representation. + + This function should be used when the simple str(o) output would result in + too general data. E.g. "<type 'instance'" is less meaningful than + "instance: Foo". + + Keyword arguments: + verbosity -- if True the first row is treated as a table header + + """ + res = "" + + t = type(o) + if (verbosity == 0) or (t not in representations): + res = str(t) + else: + verbosity -= 1 + if len(representations[t]) <= verbosity: + verbosity = len(representations[t]) - 1 + res = representations[t][verbosity](o) + + res = address.sub('', res) + res = type_repr.sub(r'\2', res) + + return res + + +def _traverse(summary, function, *args): + """Traverse all objects of a summary and call function with each as a + parameter. + + Using this function, the following objects will be traversed: + - the summary + - each row + - each item of a row + """ + function(summary, *args) + for row in summary: + function(row, *args) + for item in row: + function(item, *args) + + +def _subtract(summary, o): + """Remove object o from the summary by subtracting it's size.""" + found = False + row = [_repr(o), 1, getsizeof(o)] + for r in summary: + if r[0] == row[0]: + (r[1], r[2]) = (r[1] - row[1], r[2] - row[2]) + found = True + if not found: + summary.append([row[0], -row[1], -row[2]]) + return summary + + +def _sweep(summary): + """Remove all rows in which the total size and the total number of + objects is zero. + + """ + return [row for row in summary if ((row[2] != 0) or (row[1] != 0))] diff --git a/venv/lib/python3.9/site-packages/pympler/templates/asized_referents.tpl b/venv/lib/python3.9/site-packages/pympler/templates/asized_referents.tpl new file mode 100644 index 00000000..6271a4ce --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/asized_referents.tpl @@ -0,0 +1,9 @@ +%for ref in referents: + <div class="referents"> + <span class="local_name">{{ref.name}}</span> + <span class="local_size">{{ref.size}}</span> + %if ref.refs: + %include('asized_referents', referents=ref.refs) + %end + </div> +%end diff --git a/venv/lib/python3.9/site-packages/pympler/templates/footer.tpl b/venv/lib/python3.9/site-packages/pympler/templates/footer.tpl new file mode 100644 index 00000000..e1285c43 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/footer.tpl @@ -0,0 +1,6 @@ +</div> +</div> +</div> +</div> +</body> +</html> diff --git a/venv/lib/python3.9/site-packages/pympler/templates/garbage.tpl b/venv/lib/python3.9/site-packages/pympler/templates/garbage.tpl new file mode 100644 index 00000000..2aabca6c --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/garbage.tpl @@ -0,0 +1,33 @@ +%include('header', category='Garbage', title='Garbage') + +<h1>Garbage - Cycle {{index}}</h1> +<table class="tdata" width="100%"> + <thead> + <tr> + <th>id</th> + <th class="num">size</th> + <th>type</th> + <th>representation</th> + </tr> + </thead> + <tbody> + %for o in objects: + <tr> + <td>{{'0x%08x' % o.id}}</td> + <td class="num">{{o.size}}</td> + <td>{{o.type}}</td> + <td>{{o.str}}</td> + </tr> + %end + </tbody> +</table> + +<h2>Reference graph</h2> + +<img src="/garbage/graph/{{index}}"/> + +<h2>Reduced reference graph (cycles only)</h2> + +<img src="/garbage/graph/{{index}}?reduce=1"/> + +%include('footer') diff --git a/venv/lib/python3.9/site-packages/pympler/templates/garbage_index.tpl b/venv/lib/python3.9/site-packages/pympler/templates/garbage_index.tpl new file mode 100644 index 00000000..7fbd1930 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/garbage_index.tpl @@ -0,0 +1,41 @@ +%include('header', category='Garbage', title='Garbage') +<h1>Garbage - Overview</h1> + +<p>This page gives an overview of all objects that would have been +deleted if those weren't holding circular references to each other +(e.g. in a doubly linked list).</p> + +%if len(graphs): + <p>Click on the reference graph titles below to show the objects + contained in the respective cycle. If you have <a + href="http://www.graphviz.org">graphviz</a> installed, you will + also see a visualisation of the reference cycle.</p> + + <p>{{len(graphs)}} reference cycles:</p> + + <table class="tdata"> + <thead> + <tr> + <th>Reference graph</th> + <th># objects</th> + <th># cycle objects</th> + <th>Total size</th> + </tr> + </thead> + <tbody> + %for graph in graphs: + <tr> + <td><a href="/garbage/{{graph.index}}">Cycle {{graph.index}}</a></td> + <td class="num">{{len(graph.metadata)}}</td> + <td class="num">{{graph.num_in_cycles}}</td> + <td class="num">{{graph.total_size}}</td> + </tr> + %end + </tbody> + </table> + +%else: + <p>No reference cycles detected.</p> +%end + +%include('footer') diff --git a/venv/lib/python3.9/site-packages/pympler/templates/header.tpl b/venv/lib/python3.9/site-packages/pympler/templates/header.tpl new file mode 100644 index 00000000..34ee1761 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/header.tpl @@ -0,0 +1,36 @@ +<html> + +<head> + <title>Pympler - {{title}}</title> + <link rel="stylesheet" type="text/css" href="/static/style.css"> + <script src="http://code.jquery.com/jquery-1.10.1.min.js" type="text/javascript"></script> +</head> + +%navbar = [ +% ("overview", "/", ""), +% ("|", "", ""), +% ("process", "/process", ""), +% ("|", "", ""), +% ("tracked objects", "/tracker", ""), +% ("|", "", ""), +% ("garbage", "/garbage", ""), +% ("help", "/help", "right"),] + +<body> +<div class="related"> + <ul> + %for link, href, cls in navbar: + <li class="{{cls}}"> + %if bool(href): + <a href="{{href}}"><span>{{link}}</span></a> + %else: + <span>{{link}}</span> + %end + </li> + %end + </ul> +</div> +<div class="document"> +<div class="documentwrapper"> +<div class="bodywrapper"> +<div class="body"> diff --git a/venv/lib/python3.9/site-packages/pympler/templates/index.tpl b/venv/lib/python3.9/site-packages/pympler/templates/index.tpl new file mode 100644 index 00000000..21e6b624 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/index.tpl @@ -0,0 +1,26 @@ +%include('header', category='Overview', title='Overview') + +%from pympler.util.stringutils import pp + +<h1>Python application memory profile</h1> + +<h2>Process overview</h2> + +<table class="tdata"> + <tbody> + <tr> + <th>Virtual size:</th> + <td class="num">{{pp(processinfo.vsz)}}</td> + </tr> + <tr> + <th>Physical memory size:</th> + <td class="num">{{pp(processinfo.rss)}}</td> + </tr> + <tr> + <th>Major pagefaults:</th> + <td class="num">{{processinfo.pagefaults}}</td> + </tr> + </tbody> +</table> + +%include('footer') diff --git a/venv/lib/python3.9/site-packages/pympler/templates/jquery.flot.min.js b/venv/lib/python3.9/site-packages/pympler/templates/jquery.flot.min.js new file mode 100644 index 00000000..968d3ebd --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/jquery.flot.min.js @@ -0,0 +1,8 @@ +/* Javascript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ +(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i<c.length;++i)o[c.charAt(i)]+=d;return o.normalize()};o.scale=function(c,f){for(var i=0;i<c.length;++i)o[c.charAt(i)]*=f;return o.normalize()};o.toString=function(){if(o.a>=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return value<min?min:value>max?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);(function($){var hasOwnProperty=Object.prototype.hasOwnProperty;if(!$.fn.detach){$.fn.detach=function(){return this.each(function(){if(this.parentNode){this.parentNode.removeChild(this)}})}}function Canvas(cls,container){var element=container.children("."+cls)[0];if(element==null){element=document.createElement("canvas");element.className=cls;$(element).css({direction:"ltr",position:"absolute",left:0,top:0}).appendTo(container);if(!element.getContext){if(window.G_vmlCanvasManager){element=window.G_vmlCanvasManager.initElement(element)}else{throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode.")}}}this.element=element;var context=this.context=element.getContext("2d");var devicePixelRatio=window.devicePixelRatio||1,backingStoreRatio=context.webkitBackingStorePixelRatio||context.mozBackingStorePixelRatio||context.msBackingStorePixelRatio||context.oBackingStorePixelRatio||context.backingStorePixelRatio||1;this.pixelRatio=devicePixelRatio/backingStoreRatio;this.resize(container.width(),container.height());this.textContainer=null;this.text={};this._textCache={}}Canvas.prototype.resize=function(width,height){if(width<=0||height<=0){throw new Error("Invalid dimensions for plot, width = "+width+", height = "+height)}var element=this.element,context=this.context,pixelRatio=this.pixelRatio;if(this.width!=width){element.width=width*pixelRatio;element.style.width=width+"px";this.width=width}if(this.height!=height){element.height=height*pixelRatio;element.style.height=height+"px";this.height=height}context.restore();context.save();context.scale(pixelRatio,pixelRatio)};Canvas.prototype.clear=function(){this.context.clearRect(0,0,this.width,this.height)};Canvas.prototype.render=function(){var cache=this._textCache;for(var layerKey in cache){if(hasOwnProperty.call(cache,layerKey)){var layer=this.getTextLayer(layerKey),layerCache=cache[layerKey];layer.hide();for(var styleKey in layerCache){if(hasOwnProperty.call(layerCache,styleKey)){var styleCache=layerCache[styleKey];for(var key in styleCache){if(hasOwnProperty.call(styleCache,key)){var positions=styleCache[key].positions;for(var i=0,position;position=positions[i];i++){if(position.active){if(!position.rendered){layer.append(position.element);position.rendered=true}}else{positions.splice(i--,1);if(position.rendered){position.element.detach()}}}if(positions.length==0){delete styleCache[key]}}}}}layer.show()}}};Canvas.prototype.getTextLayer=function(classes){var layer=this.text[classes];if(layer==null){if(this.textContainer==null){this.textContainer=$("<div class='flot-text'></div>").css({position:"absolute",top:0,left:0,bottom:0,right:0,"font-size":"smaller",color:"#545454"}).insertAfter(this.element)}layer=this.text[classes]=$("<div></div>").addClass(classes).css({position:"absolute",top:0,left:0,bottom:0,right:0}).appendTo(this.textContainer)}return layer};Canvas.prototype.getTextInfo=function(layer,text,font,angle,width){var textStyle,layerCache,styleCache,info;text=""+text;if(typeof font==="object"){textStyle=font.style+" "+font.variant+" "+font.weight+" "+font.size+"px/"+font.lineHeight+"px "+font.family}else{textStyle=font}layerCache=this._textCache[layer];if(layerCache==null){layerCache=this._textCache[layer]={}}styleCache=layerCache[textStyle];if(styleCache==null){styleCache=layerCache[textStyle]={}}info=styleCache[text];if(info==null){var element=$("<div></div>").html(text).css({position:"absolute","max-width":width,top:-9999}).appendTo(this.getTextLayer(layer));if(typeof font==="object"){element.css({font:textStyle,color:font.color})}else if(typeof font==="string"){element.addClass(font)}info=styleCache[text]={width:element.outerWidth(true),height:element.outerHeight(true),element:element,positions:[]};element.detach()}return info};Canvas.prototype.addText=function(layer,x,y,text,font,angle,width,halign,valign){var info=this.getTextInfo(layer,text,font,angle,width),positions=info.positions;if(halign=="center"){x-=info.width/2}else if(halign=="right"){x-=info.width}if(valign=="middle"){y-=info.height/2}else if(valign=="bottom"){y-=info.height}for(var i=0,position;position=positions[i];i++){if(position.x==x&&position.y==y){position.active=true;return}}position={active:true,rendered:false,element:positions.length?info.element.clone():info.element,x:x,y:y};positions.push(position);position.element.css({top:Math.round(y),left:Math.round(x),"text-align":halign})};Canvas.prototype.removeText=function(layer,x,y,text,font,angle){if(text==null){var layerCache=this._textCache[layer];if(layerCache!=null){for(var styleKey in layerCache){if(hasOwnProperty.call(layerCache,styleKey)){var styleCache=layerCache[styleKey];for(var key in styleCache){if(hasOwnProperty.call(styleCache,key)){var positions=styleCache[key].positions;for(var i=0,position;position=positions[i];i++){position.active=false}}}}}}}else{var positions=this.getTextInfo(layer,text,font,angle).positions;for(var i=0,position;position=positions[i];i++){if(position.x==x&&position.y==y){position.active=false}}}};function Plot(placeholder,data_,options_,plugins){var series=[],options={colors:["#edc240","#afd8f8","#cb4b4b","#4da74d","#9440ed"],legend:{show:true,noColumns:1,labelFormatter:null,labelBoxBorderColor:"#ccc",container:null,position:"ne",margin:5,backgroundColor:null,backgroundOpacity:.85,sorted:null},xaxis:{show:null,position:"bottom",mode:null,font:null,color:null,tickColor:null,transform:null,inverseTransform:null,min:null,max:null,autoscaleMargin:null,ticks:null,tickFormatter:null,labelWidth:null,labelHeight:null,reserveSpace:null,tickLength:null,alignTicksWithAxis:null,tickDecimals:null,tickSize:null,minTickSize:null},yaxis:{autoscaleMargin:.02,position:"left"},xaxes:[],yaxes:[],series:{points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#ffffff",symbol:"circle"},lines:{lineWidth:2,fill:false,fillColor:null,steps:false},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,align:"left",horizontal:false,zero:true},shadowSize:3,highlightColor:null},grid:{show:true,aboveData:false,color:"#545454",backgroundColor:null,borderColor:null,tickColor:null,margin:0,labelMargin:5,axisMargin:8,borderWidth:2,minBorderMargin:null,markings:null,markingsColor:"#f4f4f4",markingsLineWidth:2,clickable:false,hoverable:false,autoHighlight:true,mouseActiveRadius:10},interaction:{redrawOverlayInterval:1e3/60},hooks:{}},surface=null,overlay=null,eventHolder=null,ctx=null,octx=null,xaxes=[],yaxes=[],plotOffset={left:0,right:0,top:0,bottom:0},plotWidth=0,plotHeight=0,hooks={processOptions:[],processRawData:[],processDatapoints:[],processOffset:[],drawBackground:[],drawSeries:[],draw:[],bindEvents:[],drawOverlay:[],shutdown:[]},plot=this;plot.setData=setData;plot.setupGrid=setupGrid;plot.draw=draw;plot.getPlaceholder=function(){return placeholder};plot.getCanvas=function(){return surface.element};plot.getPlotOffset=function(){return plotOffset};plot.width=function(){return plotWidth};plot.height=function(){return plotHeight};plot.offset=function(){var o=eventHolder.offset();o.left+=plotOffset.left;o.top+=plotOffset.top;return o};plot.getData=function(){return series};plot.getAxes=function(){var res={},i;$.each(xaxes.concat(yaxes),function(_,axis){if(axis)res[axis.direction+(axis.n!=1?axis.n:"")+"axis"]=axis});return res};plot.getXAxes=function(){return xaxes};plot.getYAxes=function(){return yaxes};plot.c2p=canvasToAxisCoords;plot.p2c=axisToCanvasCoords;plot.getOptions=function(){return options};plot.highlight=highlight;plot.unhighlight=unhighlight;plot.triggerRedrawOverlay=triggerRedrawOverlay;plot.pointOffset=function(point){return{left:parseInt(xaxes[axisNumber(point,"x")-1].p2c(+point.x)+plotOffset.left,10),top:parseInt(yaxes[axisNumber(point,"y")-1].p2c(+point.y)+plotOffset.top,10)}};plot.shutdown=shutdown;plot.destroy=function(){shutdown();placeholder.removeData("plot").empty();series=[];options=null;surface=null;overlay=null;eventHolder=null;ctx=null;octx=null;xaxes=[];yaxes=[];hooks=null;highlights=[];plot=null};plot.resize=function(){var width=placeholder.width(),height=placeholder.height();surface.resize(width,height);overlay.resize(width,height)};plot.hooks=hooks;initPlugins(plot);parseOptions(options_);setupCanvases();setData(data_);setupGrid();draw();bindEvents();function executeHooks(hook,args){args=[plot].concat(args);for(var i=0;i<hook.length;++i)hook[i].apply(this,args)}function initPlugins(){var classes={Canvas:Canvas};for(var i=0;i<plugins.length;++i){var p=plugins[i];p.init(plot,classes);if(p.options)$.extend(true,options,p.options)}}function parseOptions(opts){$.extend(true,options,opts);if(opts&&opts.colors){options.colors=opts.colors}if(options.xaxis.color==null)options.xaxis.color=$.color.parse(options.grid.color).scale("a",.22).toString();if(options.yaxis.color==null)options.yaxis.color=$.color.parse(options.grid.color).scale("a",.22).toString();if(options.xaxis.tickColor==null)options.xaxis.tickColor=options.grid.tickColor||options.xaxis.color;if(options.yaxis.tickColor==null)options.yaxis.tickColor=options.grid.tickColor||options.yaxis.color;if(options.grid.borderColor==null)options.grid.borderColor=options.grid.color;if(options.grid.tickColor==null)options.grid.tickColor=$.color.parse(options.grid.color).scale("a",.22).toString();var i,axisOptions,axisCount,fontSize=placeholder.css("font-size"),fontSizeDefault=fontSize?+fontSize.replace("px",""):13,fontDefaults={style:placeholder.css("font-style"),size:Math.round(.8*fontSizeDefault),variant:placeholder.css("font-variant"),weight:placeholder.css("font-weight"),family:placeholder.css("font-family")};axisCount=options.xaxes.length||1;for(i=0;i<axisCount;++i){axisOptions=options.xaxes[i];if(axisOptions&&!axisOptions.tickColor){axisOptions.tickColor=axisOptions.color}axisOptions=$.extend(true,{},options.xaxis,axisOptions);options.xaxes[i]=axisOptions;if(axisOptions.font){axisOptions.font=$.extend({},fontDefaults,axisOptions.font);if(!axisOptions.font.color){axisOptions.font.color=axisOptions.color}if(!axisOptions.font.lineHeight){axisOptions.font.lineHeight=Math.round(axisOptions.font.size*1.15)}}}axisCount=options.yaxes.length||1;for(i=0;i<axisCount;++i){axisOptions=options.yaxes[i];if(axisOptions&&!axisOptions.tickColor){axisOptions.tickColor=axisOptions.color}axisOptions=$.extend(true,{},options.yaxis,axisOptions);options.yaxes[i]=axisOptions;if(axisOptions.font){axisOptions.font=$.extend({},fontDefaults,axisOptions.font);if(!axisOptions.font.color){axisOptions.font.color=axisOptions.color}if(!axisOptions.font.lineHeight){axisOptions.font.lineHeight=Math.round(axisOptions.font.size*1.15)}}}if(options.xaxis.noTicks&&options.xaxis.ticks==null)options.xaxis.ticks=options.xaxis.noTicks;if(options.yaxis.noTicks&&options.yaxis.ticks==null)options.yaxis.ticks=options.yaxis.noTicks;if(options.x2axis){options.xaxes[1]=$.extend(true,{},options.xaxis,options.x2axis);options.xaxes[1].position="top";if(options.x2axis.min==null){options.xaxes[1].min=null}if(options.x2axis.max==null){options.xaxes[1].max=null}}if(options.y2axis){options.yaxes[1]=$.extend(true,{},options.yaxis,options.y2axis);options.yaxes[1].position="right";if(options.y2axis.min==null){options.yaxes[1].min=null}if(options.y2axis.max==null){options.yaxes[1].max=null}}if(options.grid.coloredAreas)options.grid.markings=options.grid.coloredAreas;if(options.grid.coloredAreasColor)options.grid.markingsColor=options.grid.coloredAreasColor;if(options.lines)$.extend(true,options.series.lines,options.lines);if(options.points)$.extend(true,options.series.points,options.points);if(options.bars)$.extend(true,options.series.bars,options.bars);if(options.shadowSize!=null)options.series.shadowSize=options.shadowSize;if(options.highlightColor!=null)options.series.highlightColor=options.highlightColor;for(i=0;i<options.xaxes.length;++i)getOrCreateAxis(xaxes,i+1).options=options.xaxes[i];for(i=0;i<options.yaxes.length;++i)getOrCreateAxis(yaxes,i+1).options=options.yaxes[i];for(var n in hooks)if(options.hooks[n]&&options.hooks[n].length)hooks[n]=hooks[n].concat(options.hooks[n]);executeHooks(hooks.processOptions,[options])}function setData(d){series=parseData(d);fillInSeriesOptions();processData()}function parseData(d){var res=[];for(var i=0;i<d.length;++i){var s=$.extend(true,{},options.series);if(d[i].data!=null){s.data=d[i].data;delete d[i].data;$.extend(true,s,d[i]);d[i].data=s.data}else s.data=d[i];res.push(s)}return res}function axisNumber(obj,coord){var a=obj[coord+"axis"];if(typeof a=="object")a=a.n;if(typeof a!="number")a=1;return a}function allAxes(){return $.grep(xaxes.concat(yaxes),function(a){return a})}function canvasToAxisCoords(pos){var res={},i,axis;for(i=0;i<xaxes.length;++i){axis=xaxes[i];if(axis&&axis.used)res["x"+axis.n]=axis.c2p(pos.left)}for(i=0;i<yaxes.length;++i){axis=yaxes[i];if(axis&&axis.used)res["y"+axis.n]=axis.c2p(pos.top)}if(res.x1!==undefined)res.x=res.x1;if(res.y1!==undefined)res.y=res.y1;return res}function axisToCanvasCoords(pos){var res={},i,axis,key;for(i=0;i<xaxes.length;++i){axis=xaxes[i];if(axis&&axis.used){key="x"+axis.n;if(pos[key]==null&&axis.n==1)key="x";if(pos[key]!=null){res.left=axis.p2c(pos[key]);break}}}for(i=0;i<yaxes.length;++i){axis=yaxes[i];if(axis&&axis.used){key="y"+axis.n;if(pos[key]==null&&axis.n==1)key="y";if(pos[key]!=null){res.top=axis.p2c(pos[key]);break}}}return res}function getOrCreateAxis(axes,number){if(!axes[number-1])axes[number-1]={n:number,direction:axes==xaxes?"x":"y",options:$.extend(true,{},axes==xaxes?options.xaxis:options.yaxis)};return axes[number-1]}function fillInSeriesOptions(){var neededColors=series.length,maxIndex=-1,i;for(i=0;i<series.length;++i){var sc=series[i].color;if(sc!=null){neededColors--;if(typeof sc=="number"&&sc>maxIndex){maxIndex=sc}}}if(neededColors<=maxIndex){neededColors=maxIndex+1}var c,colors=[],colorPool=options.colors,colorPoolSize=colorPool.length,variation=0;for(i=0;i<neededColors;i++){c=$.color.parse(colorPool[i%colorPoolSize]||"#666");if(i%colorPoolSize==0&&i){if(variation>=0){if(variation<.5){variation=-variation-.2}else variation=0}else variation=-variation}colors[i]=c.scale("rgb",1+variation)}var colori=0,s;for(i=0;i<series.length;++i){s=series[i];if(s.color==null){s.color=colors[colori].toString();++colori}else if(typeof s.color=="number")s.color=colors[s.color].toString();if(s.lines.show==null){var v,show=true;for(v in s)if(s[v]&&s[v].show){show=false;break}if(show)s.lines.show=true}if(s.lines.zero==null){s.lines.zero=!!s.lines.fill}s.xaxis=getOrCreateAxis(xaxes,axisNumber(s,"x"));s.yaxis=getOrCreateAxis(yaxes,axisNumber(s,"y"))}}function processData(){var topSentry=Number.POSITIVE_INFINITY,bottomSentry=Number.NEGATIVE_INFINITY,fakeInfinity=Number.MAX_VALUE,i,j,k,m,length,s,points,ps,x,y,axis,val,f,p,data,format;function updateAxis(axis,min,max){if(min<axis.datamin&&min!=-fakeInfinity)axis.datamin=min;if(max>axis.datamax&&max!=fakeInfinity)axis.datamax=max}$.each(allAxes(),function(_,axis){axis.datamin=topSentry;axis.datamax=bottomSentry;axis.used=false});for(i=0;i<series.length;++i){s=series[i];s.datapoints={points:[]};executeHooks(hooks.processRawData,[s,s.data,s.datapoints])}for(i=0;i<series.length;++i){s=series[i];data=s.data;format=s.datapoints.format;if(!format){format=[];format.push({x:true,number:true,required:true});format.push({y:true,number:true,required:true});if(s.bars.show||s.lines.show&&s.lines.fill){var autoscale=!!(s.bars.show&&s.bars.zero||s.lines.show&&s.lines.zero);format.push({y:true,number:true,required:false,defaultValue:0,autoscale:autoscale});if(s.bars.horizontal){delete format[format.length-1].y;format[format.length-1].x=true}}s.datapoints.format=format}if(s.datapoints.pointsize!=null)continue;s.datapoints.pointsize=format.length;ps=s.datapoints.pointsize;points=s.datapoints.points;var insertSteps=s.lines.show&&s.lines.steps;s.xaxis.used=s.yaxis.used=true;for(j=k=0;j<data.length;++j,k+=ps){p=data[j];var nullify=p==null;if(!nullify){for(m=0;m<ps;++m){val=p[m];f=format[m];if(f){if(f.number&&val!=null){val=+val;if(isNaN(val))val=null;else if(val==Infinity)val=fakeInfinity;else if(val==-Infinity)val=-fakeInfinity}if(val==null){if(f.required)nullify=true;if(f.defaultValue!=null)val=f.defaultValue}}points[k+m]=val}}if(nullify){for(m=0;m<ps;++m){val=points[k+m];if(val!=null){f=format[m];if(f.autoscale!==false){if(f.x){updateAxis(s.xaxis,val,val)}if(f.y){updateAxis(s.yaxis,val,val)}}}points[k+m]=null}}else{if(insertSteps&&k>0&&points[k-ps]!=null&&points[k-ps]!=points[k]&&points[k-ps+1]!=points[k+1]){for(m=0;m<ps;++m)points[k+ps+m]=points[k+m];points[k+1]=points[k-ps+1];k+=ps}}}}for(i=0;i<series.length;++i){s=series[i];executeHooks(hooks.processDatapoints,[s,s.datapoints])}for(i=0;i<series.length;++i){s=series[i];points=s.datapoints.points;ps=s.datapoints.pointsize;format=s.datapoints.format;var xmin=topSentry,ymin=topSentry,xmax=bottomSentry,ymax=bottomSentry;for(j=0;j<points.length;j+=ps){if(points[j]==null)continue;for(m=0;m<ps;++m){val=points[j+m];f=format[m];if(!f||f.autoscale===false||val==fakeInfinity||val==-fakeInfinity)continue;if(f.x){if(val<xmin)xmin=val;if(val>xmax)xmax=val}if(f.y){if(val<ymin)ymin=val;if(val>ymax)ymax=val}}}if(s.bars.show){var delta;switch(s.bars.align){case"left":delta=0;break;case"right":delta=-s.bars.barWidth;break;default:delta=-s.bars.barWidth/2}if(s.bars.horizontal){ymin+=delta;ymax+=delta+s.bars.barWidth}else{xmin+=delta;xmax+=delta+s.bars.barWidth}}updateAxis(s.xaxis,xmin,xmax);updateAxis(s.yaxis,ymin,ymax)}$.each(allAxes(),function(_,axis){if(axis.datamin==topSentry)axis.datamin=null;if(axis.datamax==bottomSentry)axis.datamax=null})}function setupCanvases(){placeholder.css("padding",0).children().filter(function(){return!$(this).hasClass("flot-overlay")&&!$(this).hasClass("flot-base")}).remove();if(placeholder.css("position")=="static")placeholder.css("position","relative");surface=new Canvas("flot-base",placeholder);overlay=new Canvas("flot-overlay",placeholder);ctx=surface.context;octx=overlay.context;eventHolder=$(overlay.element).unbind();var existing=placeholder.data("plot");if(existing){existing.shutdown();overlay.clear()}placeholder.data("plot",plot)}function bindEvents(){if(options.grid.hoverable){eventHolder.mousemove(onMouseMove);eventHolder.bind("mouseleave",onMouseLeave)}if(options.grid.clickable)eventHolder.click(onClick);executeHooks(hooks.bindEvents,[eventHolder])}function shutdown(){if(redrawTimeout)clearTimeout(redrawTimeout);eventHolder.unbind("mousemove",onMouseMove);eventHolder.unbind("mouseleave",onMouseLeave);eventHolder.unbind("click",onClick);executeHooks(hooks.shutdown,[eventHolder])}function setTransformationHelpers(axis){function identity(x){return x}var s,m,t=axis.options.transform||identity,it=axis.options.inverseTransform;if(axis.direction=="x"){s=axis.scale=plotWidth/Math.abs(t(axis.max)-t(axis.min));m=Math.min(t(axis.max),t(axis.min))}else{s=axis.scale=plotHeight/Math.abs(t(axis.max)-t(axis.min));s=-s;m=Math.max(t(axis.max),t(axis.min))}if(t==identity)axis.p2c=function(p){return(p-m)*s};else axis.p2c=function(p){return(t(p)-m)*s};if(!it)axis.c2p=function(c){return m+c/s};else axis.c2p=function(c){return it(m+c/s)}}function measureTickLabels(axis){var opts=axis.options,ticks=axis.ticks||[],labelWidth=opts.labelWidth||0,labelHeight=opts.labelHeight||0,maxWidth=labelWidth||(axis.direction=="x"?Math.floor(surface.width/(ticks.length||1)):null),legacyStyles=axis.direction+"Axis "+axis.direction+axis.n+"Axis",layer="flot-"+axis.direction+"-axis flot-"+axis.direction+axis.n+"-axis "+legacyStyles,font=opts.font||"flot-tick-label tickLabel";for(var i=0;i<ticks.length;++i){var t=ticks[i];if(!t.label)continue;var info=surface.getTextInfo(layer,t.label,font,null,maxWidth);labelWidth=Math.max(labelWidth,info.width);labelHeight=Math.max(labelHeight,info.height)}axis.labelWidth=opts.labelWidth||labelWidth;axis.labelHeight=opts.labelHeight||labelHeight}function allocateAxisBoxFirstPhase(axis){var lw=axis.labelWidth,lh=axis.labelHeight,pos=axis.options.position,isXAxis=axis.direction==="x",tickLength=axis.options.tickLength,axisMargin=options.grid.axisMargin,padding=options.grid.labelMargin,innermost=true,outermost=true,first=true,found=false;$.each(isXAxis?xaxes:yaxes,function(i,a){if(a&&(a.show||a.reserveSpace)){if(a===axis){found=true}else if(a.options.position===pos){if(found){outermost=false}else{innermost=false}}if(!found){first=false}}});if(outermost){axisMargin=0}if(tickLength==null){tickLength=first?"full":5}if(!isNaN(+tickLength))padding+=+tickLength;if(isXAxis){lh+=padding;if(pos=="bottom"){plotOffset.bottom+=lh+axisMargin;axis.box={top:surface.height-plotOffset.bottom,height:lh}}else{axis.box={top:plotOffset.top+axisMargin,height:lh};plotOffset.top+=lh+axisMargin}}else{lw+=padding;if(pos=="left"){axis.box={left:plotOffset.left+axisMargin,width:lw};plotOffset.left+=lw+axisMargin}else{plotOffset.right+=lw+axisMargin;axis.box={left:surface.width-plotOffset.right,width:lw}}}axis.position=pos;axis.tickLength=tickLength;axis.box.padding=padding;axis.innermost=innermost}function allocateAxisBoxSecondPhase(axis){if(axis.direction=="x"){axis.box.left=plotOffset.left-axis.labelWidth/2;axis.box.width=surface.width-plotOffset.left-plotOffset.right+axis.labelWidth}else{axis.box.top=plotOffset.top-axis.labelHeight/2;axis.box.height=surface.height-plotOffset.bottom-plotOffset.top+axis.labelHeight}}function adjustLayoutForThingsStickingOut(){var minMargin=options.grid.minBorderMargin,axis,i;if(minMargin==null){minMargin=0;for(i=0;i<series.length;++i)minMargin=Math.max(minMargin,2*(series[i].points.radius+series[i].points.lineWidth/2))}var margins={left:minMargin,right:minMargin,top:minMargin,bottom:minMargin};$.each(allAxes(),function(_,axis){if(axis.reserveSpace&&axis.ticks&&axis.ticks.length){if(axis.direction==="x"){margins.left=Math.max(margins.left,axis.labelWidth/2);margins.right=Math.max(margins.right,axis.labelWidth/2)}else{margins.bottom=Math.max(margins.bottom,axis.labelHeight/2);margins.top=Math.max(margins.top,axis.labelHeight/2)}}});plotOffset.left=Math.ceil(Math.max(margins.left,plotOffset.left));plotOffset.right=Math.ceil(Math.max(margins.right,plotOffset.right));plotOffset.top=Math.ceil(Math.max(margins.top,plotOffset.top));plotOffset.bottom=Math.ceil(Math.max(margins.bottom,plotOffset.bottom))}function setupGrid(){var i,axes=allAxes(),showGrid=options.grid.show;for(var a in plotOffset){var margin=options.grid.margin||0;plotOffset[a]=typeof margin=="number"?margin:margin[a]||0}executeHooks(hooks.processOffset,[plotOffset]);for(var a in plotOffset){if(typeof options.grid.borderWidth=="object"){plotOffset[a]+=showGrid?options.grid.borderWidth[a]:0}else{plotOffset[a]+=showGrid?options.grid.borderWidth:0}}$.each(axes,function(_,axis){var axisOpts=axis.options;axis.show=axisOpts.show==null?axis.used:axisOpts.show;axis.reserveSpace=axisOpts.reserveSpace==null?axis.show:axisOpts.reserveSpace;setRange(axis)});if(showGrid){var allocatedAxes=$.grep(axes,function(axis){return axis.show||axis.reserveSpace});$.each(allocatedAxes,function(_,axis){setupTickGeneration(axis);setTicks(axis);snapRangeToTicks(axis,axis.ticks);measureTickLabels(axis)});for(i=allocatedAxes.length-1;i>=0;--i)allocateAxisBoxFirstPhase(allocatedAxes[i]);adjustLayoutForThingsStickingOut();$.each(allocatedAxes,function(_,axis){allocateAxisBoxSecondPhase(axis)})}plotWidth=surface.width-plotOffset.left-plotOffset.right;plotHeight=surface.height-plotOffset.bottom-plotOffset.top;$.each(axes,function(_,axis){setTransformationHelpers(axis)});if(showGrid){drawAxisLabels()}insertLegend()}function setRange(axis){var opts=axis.options,min=+(opts.min!=null?opts.min:axis.datamin),max=+(opts.max!=null?opts.max:axis.datamax),delta=max-min;if(delta==0){var widen=max==0?1:.01;if(opts.min==null)min-=widen;if(opts.max==null||opts.min!=null)max+=widen}else{var margin=opts.autoscaleMargin;if(margin!=null){if(opts.min==null){min-=delta*margin;if(min<0&&axis.datamin!=null&&axis.datamin>=0)min=0}if(opts.max==null){max+=delta*margin;if(max>0&&axis.datamax!=null&&axis.datamax<=0)max=0}}}axis.min=min;axis.max=max}function setupTickGeneration(axis){var opts=axis.options;var noTicks;if(typeof opts.ticks=="number"&&opts.ticks>0)noTicks=opts.ticks;else noTicks=.3*Math.sqrt(axis.direction=="x"?surface.width:surface.height);var delta=(axis.max-axis.min)/noTicks,dec=-Math.floor(Math.log(delta)/Math.LN10),maxDec=opts.tickDecimals;if(maxDec!=null&&dec>maxDec){dec=maxDec}var magn=Math.pow(10,-dec),norm=delta/magn,size;if(norm<1.5){size=1}else if(norm<3){size=2;if(norm>2.25&&(maxDec==null||dec+1<=maxDec)){size=2.5;++dec}}else if(norm<7.5){size=5}else{size=10}size*=magn;if(opts.minTickSize!=null&&size<opts.minTickSize){size=opts.minTickSize}axis.delta=delta;axis.tickDecimals=Math.max(0,maxDec!=null?maxDec:dec);axis.tickSize=opts.tickSize||size;if(opts.mode=="time"&&!axis.tickGenerator){throw new Error("Time mode requires the flot.time plugin.")}if(!axis.tickGenerator){axis.tickGenerator=function(axis){var ticks=[],start=floorInBase(axis.min,axis.tickSize),i=0,v=Number.NaN,prev;do{prev=v;v=start+i*axis.tickSize;ticks.push(v);++i}while(v<axis.max&&v!=prev);return ticks};axis.tickFormatter=function(value,axis){var factor=axis.tickDecimals?Math.pow(10,axis.tickDecimals):1;var formatted=""+Math.round(value*factor)/factor;if(axis.tickDecimals!=null){var decimal=formatted.indexOf(".");var precision=decimal==-1?0:formatted.length-decimal-1;if(precision<axis.tickDecimals){return(precision?formatted:formatted+".")+(""+factor).substr(1,axis.tickDecimals-precision)}}return formatted}}if($.isFunction(opts.tickFormatter))axis.tickFormatter=function(v,axis){return""+opts.tickFormatter(v,axis)};if(opts.alignTicksWithAxis!=null){var otherAxis=(axis.direction=="x"?xaxes:yaxes)[opts.alignTicksWithAxis-1];if(otherAxis&&otherAxis.used&&otherAxis!=axis){var niceTicks=axis.tickGenerator(axis);if(niceTicks.length>0){if(opts.min==null)axis.min=Math.min(axis.min,niceTicks[0]);if(opts.max==null&&niceTicks.length>1)axis.max=Math.max(axis.max,niceTicks[niceTicks.length-1])}axis.tickGenerator=function(axis){var ticks=[],v,i;for(i=0;i<otherAxis.ticks.length;++i){v=(otherAxis.ticks[i].v-otherAxis.min)/(otherAxis.max-otherAxis.min);v=axis.min+v*(axis.max-axis.min);ticks.push(v)}return ticks};if(!axis.mode&&opts.tickDecimals==null){var extraDec=Math.max(0,-Math.floor(Math.log(axis.delta)/Math.LN10)+1),ts=axis.tickGenerator(axis);if(!(ts.length>1&&/\..*0$/.test((ts[1]-ts[0]).toFixed(extraDec))))axis.tickDecimals=extraDec}}}}function setTicks(axis){var oticks=axis.options.ticks,ticks=[];if(oticks==null||typeof oticks=="number"&&oticks>0)ticks=axis.tickGenerator(axis);else if(oticks){if($.isFunction(oticks))ticks=oticks(axis);else ticks=oticks}var i,v;axis.ticks=[];for(i=0;i<ticks.length;++i){var label=null;var t=ticks[i];if(typeof t=="object"){v=+t[0];if(t.length>1)label=t[1]}else v=+t;if(label==null)label=axis.tickFormatter(v,axis);if(!isNaN(v))axis.ticks.push({v:v,label:label})}}function snapRangeToTicks(axis,ticks){if(axis.options.autoscaleMargin&&ticks.length>0){if(axis.options.min==null)axis.min=Math.min(axis.min,ticks[0].v);if(axis.options.max==null&&ticks.length>1)axis.max=Math.max(axis.max,ticks[ticks.length-1].v)}}function draw(){surface.clear();executeHooks(hooks.drawBackground,[ctx]);var grid=options.grid;if(grid.show&&grid.backgroundColor)drawBackground();if(grid.show&&!grid.aboveData){drawGrid()}for(var i=0;i<series.length;++i){executeHooks(hooks.drawSeries,[ctx,series[i]]);drawSeries(series[i])}executeHooks(hooks.draw,[ctx]);if(grid.show&&grid.aboveData){drawGrid()}surface.render();triggerRedrawOverlay()}function extractRange(ranges,coord){var axis,from,to,key,axes=allAxes();for(var i=0;i<axes.length;++i){axis=axes[i];if(axis.direction==coord){key=coord+axis.n+"axis";if(!ranges[key]&&axis.n==1)key=coord+"axis";if(ranges[key]){from=ranges[key].from;to=ranges[key].to;break}}}if(!ranges[key]){axis=coord=="x"?xaxes[0]:yaxes[0];from=ranges[coord+"1"];to=ranges[coord+"2"]}if(from!=null&&to!=null&&from>to){var tmp=from;from=to;to=tmp}return{from:from,to:to,axis:axis}}function drawBackground(){ctx.save();ctx.translate(plotOffset.left,plotOffset.top);ctx.fillStyle=getColorOrGradient(options.grid.backgroundColor,plotHeight,0,"rgba(255, 255, 255, 0)");ctx.fillRect(0,0,plotWidth,plotHeight);ctx.restore()}function drawGrid(){var i,axes,bw,bc;ctx.save();ctx.translate(plotOffset.left,plotOffset.top);var markings=options.grid.markings;if(markings){if($.isFunction(markings)){axes=plot.getAxes();axes.xmin=axes.xaxis.min;axes.xmax=axes.xaxis.max;axes.ymin=axes.yaxis.min;axes.ymax=axes.yaxis.max;markings=markings(axes)}for(i=0;i<markings.length;++i){var m=markings[i],xrange=extractRange(m,"x"),yrange=extractRange(m,"y");if(xrange.from==null)xrange.from=xrange.axis.min;if(xrange.to==null)xrange.to=xrange.axis.max; +if(yrange.from==null)yrange.from=yrange.axis.min;if(yrange.to==null)yrange.to=yrange.axis.max;if(xrange.to<xrange.axis.min||xrange.from>xrange.axis.max||yrange.to<yrange.axis.min||yrange.from>yrange.axis.max)continue;xrange.from=Math.max(xrange.from,xrange.axis.min);xrange.to=Math.min(xrange.to,xrange.axis.max);yrange.from=Math.max(yrange.from,yrange.axis.min);yrange.to=Math.min(yrange.to,yrange.axis.max);var xequal=xrange.from===xrange.to,yequal=yrange.from===yrange.to;if(xequal&&yequal){continue}xrange.from=Math.floor(xrange.axis.p2c(xrange.from));xrange.to=Math.floor(xrange.axis.p2c(xrange.to));yrange.from=Math.floor(yrange.axis.p2c(yrange.from));yrange.to=Math.floor(yrange.axis.p2c(yrange.to));if(xequal||yequal){var lineWidth=m.lineWidth||options.grid.markingsLineWidth,subPixel=lineWidth%2?.5:0;ctx.beginPath();ctx.strokeStyle=m.color||options.grid.markingsColor;ctx.lineWidth=lineWidth;if(xequal){ctx.moveTo(xrange.to+subPixel,yrange.from);ctx.lineTo(xrange.to+subPixel,yrange.to)}else{ctx.moveTo(xrange.from,yrange.to+subPixel);ctx.lineTo(xrange.to,yrange.to+subPixel)}ctx.stroke()}else{ctx.fillStyle=m.color||options.grid.markingsColor;ctx.fillRect(xrange.from,yrange.to,xrange.to-xrange.from,yrange.from-yrange.to)}}}axes=allAxes();bw=options.grid.borderWidth;for(var j=0;j<axes.length;++j){var axis=axes[j],box=axis.box,t=axis.tickLength,x,y,xoff,yoff;if(!axis.show||axis.ticks.length==0)continue;ctx.lineWidth=1;if(axis.direction=="x"){x=0;if(t=="full")y=axis.position=="top"?0:plotHeight;else y=box.top-plotOffset.top+(axis.position=="top"?box.height:0)}else{y=0;if(t=="full")x=axis.position=="left"?0:plotWidth;else x=box.left-plotOffset.left+(axis.position=="left"?box.width:0)}if(!axis.innermost){ctx.strokeStyle=axis.options.color;ctx.beginPath();xoff=yoff=0;if(axis.direction=="x")xoff=plotWidth+1;else yoff=plotHeight+1;if(ctx.lineWidth==1){if(axis.direction=="x"){y=Math.floor(y)+.5}else{x=Math.floor(x)+.5}}ctx.moveTo(x,y);ctx.lineTo(x+xoff,y+yoff);ctx.stroke()}ctx.strokeStyle=axis.options.tickColor;ctx.beginPath();for(i=0;i<axis.ticks.length;++i){var v=axis.ticks[i].v;xoff=yoff=0;if(isNaN(v)||v<axis.min||v>axis.max||t=="full"&&(typeof bw=="object"&&bw[axis.position]>0||bw>0)&&(v==axis.min||v==axis.max))continue;if(axis.direction=="x"){x=axis.p2c(v);yoff=t=="full"?-plotHeight:t;if(axis.position=="top")yoff=-yoff}else{y=axis.p2c(v);xoff=t=="full"?-plotWidth:t;if(axis.position=="left")xoff=-xoff}if(ctx.lineWidth==1){if(axis.direction=="x")x=Math.floor(x)+.5;else y=Math.floor(y)+.5}ctx.moveTo(x,y);ctx.lineTo(x+xoff,y+yoff)}ctx.stroke()}if(bw){bc=options.grid.borderColor;if(typeof bw=="object"||typeof bc=="object"){if(typeof bw!=="object"){bw={top:bw,right:bw,bottom:bw,left:bw}}if(typeof bc!=="object"){bc={top:bc,right:bc,bottom:bc,left:bc}}if(bw.top>0){ctx.strokeStyle=bc.top;ctx.lineWidth=bw.top;ctx.beginPath();ctx.moveTo(0-bw.left,0-bw.top/2);ctx.lineTo(plotWidth,0-bw.top/2);ctx.stroke()}if(bw.right>0){ctx.strokeStyle=bc.right;ctx.lineWidth=bw.right;ctx.beginPath();ctx.moveTo(plotWidth+bw.right/2,0-bw.top);ctx.lineTo(plotWidth+bw.right/2,plotHeight);ctx.stroke()}if(bw.bottom>0){ctx.strokeStyle=bc.bottom;ctx.lineWidth=bw.bottom;ctx.beginPath();ctx.moveTo(plotWidth+bw.right,plotHeight+bw.bottom/2);ctx.lineTo(0,plotHeight+bw.bottom/2);ctx.stroke()}if(bw.left>0){ctx.strokeStyle=bc.left;ctx.lineWidth=bw.left;ctx.beginPath();ctx.moveTo(0-bw.left/2,plotHeight+bw.bottom);ctx.lineTo(0-bw.left/2,0);ctx.stroke()}}else{ctx.lineWidth=bw;ctx.strokeStyle=options.grid.borderColor;ctx.strokeRect(-bw/2,-bw/2,plotWidth+bw,plotHeight+bw)}}ctx.restore()}function drawAxisLabels(){$.each(allAxes(),function(_,axis){var box=axis.box,legacyStyles=axis.direction+"Axis "+axis.direction+axis.n+"Axis",layer="flot-"+axis.direction+"-axis flot-"+axis.direction+axis.n+"-axis "+legacyStyles,font=axis.options.font||"flot-tick-label tickLabel",tick,x,y,halign,valign;surface.removeText(layer);if(!axis.show||axis.ticks.length==0)return;for(var i=0;i<axis.ticks.length;++i){tick=axis.ticks[i];if(!tick.label||tick.v<axis.min||tick.v>axis.max)continue;if(axis.direction=="x"){halign="center";x=plotOffset.left+axis.p2c(tick.v);if(axis.position=="bottom"){y=box.top+box.padding}else{y=box.top+box.height-box.padding;valign="bottom"}}else{valign="middle";y=plotOffset.top+axis.p2c(tick.v);if(axis.position=="left"){x=box.left+box.width-box.padding;halign="right"}else{x=box.left+box.padding}}surface.addText(layer,x,y,tick.label,font,null,null,halign,valign)}})}function drawSeries(series){if(series.lines.show)drawSeriesLines(series);if(series.bars.show)drawSeriesBars(series);if(series.points.show)drawSeriesPoints(series)}function drawSeriesLines(series){function plotLine(datapoints,xoffset,yoffset,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize,prevx=null,prevy=null;ctx.beginPath();for(var i=ps;i<points.length;i+=ps){var x1=points[i-ps],y1=points[i-ps+1],x2=points[i],y2=points[i+1];if(x1==null||x2==null)continue;if(y1<=y2&&y1<axisy.min){if(y2<axisy.min)continue;x1=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.min}else if(y2<=y1&&y2<axisy.min){if(y1<axisy.min)continue;x2=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.min}if(y1>=y2&&y1>axisy.max){if(y2>axisy.max)continue;x1=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.max}else if(y2>=y1&&y2>axisy.max){if(y1>axisy.max)continue;x2=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.max}if(x1<=x2&&x1<axisx.min){if(x2<axisx.min)continue;y1=(axisx.min-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.min}else if(x2<=x1&&x2<axisx.min){if(x1<axisx.min)continue;y2=(axisx.min-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.min}if(x1>=x2&&x1>axisx.max){if(x2>axisx.max)continue;y1=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.max}else if(x2>=x1&&x2>axisx.max){if(x1>axisx.max)continue;y2=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.max}if(x1!=prevx||y1!=prevy)ctx.moveTo(axisx.p2c(x1)+xoffset,axisy.p2c(y1)+yoffset);prevx=x2;prevy=y2;ctx.lineTo(axisx.p2c(x2)+xoffset,axisy.p2c(y2)+yoffset)}ctx.stroke()}function plotLineArea(datapoints,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize,bottom=Math.min(Math.max(0,axisy.min),axisy.max),i=0,top,areaOpen=false,ypos=1,segmentStart=0,segmentEnd=0;while(true){if(ps>0&&i>points.length+ps)break;i+=ps;var x1=points[i-ps],y1=points[i-ps+ypos],x2=points[i],y2=points[i+ypos];if(areaOpen){if(ps>0&&x1!=null&&x2==null){segmentEnd=i;ps=-ps;ypos=2;continue}if(ps<0&&i==segmentStart+ps){ctx.fill();areaOpen=false;ps=-ps;ypos=1;i=segmentStart=segmentEnd+ps;continue}}if(x1==null||x2==null)continue;if(x1<=x2&&x1<axisx.min){if(x2<axisx.min)continue;y1=(axisx.min-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.min}else if(x2<=x1&&x2<axisx.min){if(x1<axisx.min)continue;y2=(axisx.min-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.min}if(x1>=x2&&x1>axisx.max){if(x2>axisx.max)continue;y1=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.max}else if(x2>=x1&&x2>axisx.max){if(x1>axisx.max)continue;y2=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.max}if(!areaOpen){ctx.beginPath();ctx.moveTo(axisx.p2c(x1),axisy.p2c(bottom));areaOpen=true}if(y1>=axisy.max&&y2>=axisy.max){ctx.lineTo(axisx.p2c(x1),axisy.p2c(axisy.max));ctx.lineTo(axisx.p2c(x2),axisy.p2c(axisy.max));continue}else if(y1<=axisy.min&&y2<=axisy.min){ctx.lineTo(axisx.p2c(x1),axisy.p2c(axisy.min));ctx.lineTo(axisx.p2c(x2),axisy.p2c(axisy.min));continue}var x1old=x1,x2old=x2;if(y1<=y2&&y1<axisy.min&&y2>=axisy.min){x1=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.min}else if(y2<=y1&&y2<axisy.min&&y1>=axisy.min){x2=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.min}if(y1>=y2&&y1>axisy.max&&y2<=axisy.max){x1=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.max}else if(y2>=y1&&y2>axisy.max&&y1<=axisy.max){x2=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.max}if(x1!=x1old){ctx.lineTo(axisx.p2c(x1old),axisy.p2c(y1))}ctx.lineTo(axisx.p2c(x1),axisy.p2c(y1));ctx.lineTo(axisx.p2c(x2),axisy.p2c(y2));if(x2!=x2old){ctx.lineTo(axisx.p2c(x2),axisy.p2c(y2));ctx.lineTo(axisx.p2c(x2old),axisy.p2c(y2))}}}ctx.save();ctx.translate(plotOffset.left,plotOffset.top);ctx.lineJoin="round";var lw=series.lines.lineWidth,sw=series.shadowSize;if(lw>0&&sw>0){ctx.lineWidth=sw;ctx.strokeStyle="rgba(0,0,0,0.1)";var angle=Math.PI/18;plotLine(series.datapoints,Math.sin(angle)*(lw/2+sw/2),Math.cos(angle)*(lw/2+sw/2),series.xaxis,series.yaxis);ctx.lineWidth=sw/2;plotLine(series.datapoints,Math.sin(angle)*(lw/2+sw/4),Math.cos(angle)*(lw/2+sw/4),series.xaxis,series.yaxis)}ctx.lineWidth=lw;ctx.strokeStyle=series.color;var fillStyle=getFillStyle(series.lines,series.color,0,plotHeight);if(fillStyle){ctx.fillStyle=fillStyle;plotLineArea(series.datapoints,series.xaxis,series.yaxis)}if(lw>0)plotLine(series.datapoints,0,0,series.xaxis,series.yaxis);ctx.restore()}function drawSeriesPoints(series){function plotPoints(datapoints,radius,fillStyle,offset,shadow,axisx,axisy,symbol){var points=datapoints.points,ps=datapoints.pointsize;for(var i=0;i<points.length;i+=ps){var x=points[i],y=points[i+1];if(x==null||x<axisx.min||x>axisx.max||y<axisy.min||y>axisy.max)continue;ctx.beginPath();x=axisx.p2c(x);y=axisy.p2c(y)+offset;if(symbol=="circle")ctx.arc(x,y,radius,0,shadow?Math.PI:Math.PI*2,false);else symbol(ctx,x,y,radius,shadow);ctx.closePath();if(fillStyle){ctx.fillStyle=fillStyle;ctx.fill()}ctx.stroke()}}ctx.save();ctx.translate(plotOffset.left,plotOffset.top);var lw=series.points.lineWidth,sw=series.shadowSize,radius=series.points.radius,symbol=series.points.symbol;if(lw==0)lw=1e-4;if(lw>0&&sw>0){var w=sw/2;ctx.lineWidth=w;ctx.strokeStyle="rgba(0,0,0,0.1)";plotPoints(series.datapoints,radius,null,w+w/2,true,series.xaxis,series.yaxis,symbol);ctx.strokeStyle="rgba(0,0,0,0.2)";plotPoints(series.datapoints,radius,null,w/2,true,series.xaxis,series.yaxis,symbol)}ctx.lineWidth=lw;ctx.strokeStyle=series.color;plotPoints(series.datapoints,radius,getFillStyle(series.points,series.color),0,false,series.xaxis,series.yaxis,symbol);ctx.restore()}function drawBar(x,y,b,barLeft,barRight,fillStyleCallback,axisx,axisy,c,horizontal,lineWidth){var left,right,bottom,top,drawLeft,drawRight,drawTop,drawBottom,tmp;if(horizontal){drawBottom=drawRight=drawTop=true;drawLeft=false;left=b;right=x;top=y+barLeft;bottom=y+barRight;if(right<left){tmp=right;right=left;left=tmp;drawLeft=true;drawRight=false}}else{drawLeft=drawRight=drawTop=true;drawBottom=false;left=x+barLeft;right=x+barRight;bottom=b;top=y;if(top<bottom){tmp=top;top=bottom;bottom=tmp;drawBottom=true;drawTop=false}}if(right<axisx.min||left>axisx.max||top<axisy.min||bottom>axisy.max)return;if(left<axisx.min){left=axisx.min;drawLeft=false}if(right>axisx.max){right=axisx.max;drawRight=false}if(bottom<axisy.min){bottom=axisy.min;drawBottom=false}if(top>axisy.max){top=axisy.max;drawTop=false}left=axisx.p2c(left);bottom=axisy.p2c(bottom);right=axisx.p2c(right);top=axisy.p2c(top);if(fillStyleCallback){c.fillStyle=fillStyleCallback(bottom,top);c.fillRect(left,top,right-left,bottom-top)}if(lineWidth>0&&(drawLeft||drawRight||drawTop||drawBottom)){c.beginPath();c.moveTo(left,bottom);if(drawLeft)c.lineTo(left,top);else c.moveTo(left,top);if(drawTop)c.lineTo(right,top);else c.moveTo(right,top);if(drawRight)c.lineTo(right,bottom);else c.moveTo(right,bottom);if(drawBottom)c.lineTo(left,bottom);else c.moveTo(left,bottom);c.stroke()}}function drawSeriesBars(series){function plotBars(datapoints,barLeft,barRight,fillStyleCallback,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize;for(var i=0;i<points.length;i+=ps){if(points[i]==null)continue;drawBar(points[i],points[i+1],points[i+2],barLeft,barRight,fillStyleCallback,axisx,axisy,ctx,series.bars.horizontal,series.bars.lineWidth)}}ctx.save();ctx.translate(plotOffset.left,plotOffset.top);ctx.lineWidth=series.bars.lineWidth;ctx.strokeStyle=series.color;var barLeft;switch(series.bars.align){case"left":barLeft=0;break;case"right":barLeft=-series.bars.barWidth;break;default:barLeft=-series.bars.barWidth/2}var fillStyleCallback=series.bars.fill?function(bottom,top){return getFillStyle(series.bars,series.color,bottom,top)}:null;plotBars(series.datapoints,barLeft,barLeft+series.bars.barWidth,fillStyleCallback,series.xaxis,series.yaxis);ctx.restore()}function getFillStyle(filloptions,seriesColor,bottom,top){var fill=filloptions.fill;if(!fill)return null;if(filloptions.fillColor)return getColorOrGradient(filloptions.fillColor,bottom,top,seriesColor);var c=$.color.parse(seriesColor);c.a=typeof fill=="number"?fill:.4;c.normalize();return c.toString()}function insertLegend(){if(options.legend.container!=null){$(options.legend.container).html("")}else{placeholder.find(".legend").remove()}if(!options.legend.show){return}var fragments=[],entries=[],rowStarted=false,lf=options.legend.labelFormatter,s,label;for(var i=0;i<series.length;++i){s=series[i];if(s.label){label=lf?lf(s.label,s):s.label;if(label){entries.push({label:label,color:s.color})}}}if(options.legend.sorted){if($.isFunction(options.legend.sorted)){entries.sort(options.legend.sorted)}else if(options.legend.sorted=="reverse"){entries.reverse()}else{var ascending=options.legend.sorted!="descending";entries.sort(function(a,b){return a.label==b.label?0:a.label<b.label!=ascending?1:-1})}}for(var i=0;i<entries.length;++i){var entry=entries[i];if(i%options.legend.noColumns==0){if(rowStarted)fragments.push("</tr>");fragments.push("<tr>");rowStarted=true}fragments.push('<td class="legendColorBox"><div style="border:1px solid '+options.legend.labelBoxBorderColor+';padding:1px"><div style="width:4px;height:0;border:5px solid '+entry.color+';overflow:hidden"></div></div></td>'+'<td class="legendLabel">'+entry.label+"</td>")}if(rowStarted)fragments.push("</tr>");if(fragments.length==0)return;var table='<table style="font-size:smaller;color:'+options.grid.color+'">'+fragments.join("")+"</table>";if(options.legend.container!=null)$(options.legend.container).html(table);else{var pos="",p=options.legend.position,m=options.legend.margin;if(m[0]==null)m=[m,m];if(p.charAt(0)=="n")pos+="top:"+(m[1]+plotOffset.top)+"px;";else if(p.charAt(0)=="s")pos+="bottom:"+(m[1]+plotOffset.bottom)+"px;";if(p.charAt(1)=="e")pos+="right:"+(m[0]+plotOffset.right)+"px;";else if(p.charAt(1)=="w")pos+="left:"+(m[0]+plotOffset.left)+"px;";var legend=$('<div class="legend">'+table.replace('style="','style="position:absolute;'+pos+";")+"</div>").appendTo(placeholder);if(options.legend.backgroundOpacity!=0){var c=options.legend.backgroundColor;if(c==null){c=options.grid.backgroundColor;if(c&&typeof c=="string")c=$.color.parse(c);else c=$.color.extract(legend,"background-color");c.a=1;c=c.toString()}var div=legend.children();$('<div style="position:absolute;width:'+div.width()+"px;height:"+div.height()+"px;"+pos+"background-color:"+c+';"> </div>').prependTo(legend).css("opacity",options.legend.backgroundOpacity)}}}var highlights=[],redrawTimeout=null;function findNearbyItem(mouseX,mouseY,seriesFilter){var maxDistance=options.grid.mouseActiveRadius,smallestDistance=maxDistance*maxDistance+1,item=null,foundPoint=false,i,j,ps;for(i=series.length-1;i>=0;--i){if(!seriesFilter(series[i]))continue;var s=series[i],axisx=s.xaxis,axisy=s.yaxis,points=s.datapoints.points,mx=axisx.c2p(mouseX),my=axisy.c2p(mouseY),maxx=maxDistance/axisx.scale,maxy=maxDistance/axisy.scale;ps=s.datapoints.pointsize;if(axisx.options.inverseTransform)maxx=Number.MAX_VALUE;if(axisy.options.inverseTransform)maxy=Number.MAX_VALUE;if(s.lines.show||s.points.show){for(j=0;j<points.length;j+=ps){var x=points[j],y=points[j+1];if(x==null)continue;if(x-mx>maxx||x-mx<-maxx||y-my>maxy||y-my<-maxy)continue;var dx=Math.abs(axisx.p2c(x)-mouseX),dy=Math.abs(axisy.p2c(y)-mouseY),dist=dx*dx+dy*dy;if(dist<smallestDistance){smallestDistance=dist;item=[i,j/ps]}}}if(s.bars.show&&!item){var barLeft,barRight;switch(s.bars.align){case"left":barLeft=0;break;case"right":barLeft=-s.bars.barWidth;break;default:barLeft=-s.bars.barWidth/2}barRight=barLeft+s.bars.barWidth;for(j=0;j<points.length;j+=ps){var x=points[j],y=points[j+1],b=points[j+2];if(x==null)continue;if(series[i].bars.horizontal?mx<=Math.max(b,x)&&mx>=Math.min(b,x)&&my>=y+barLeft&&my<=y+barRight:mx>=x+barLeft&&mx<=x+barRight&&my>=Math.min(b,y)&&my<=Math.max(b,y))item=[i,j/ps]}}}if(item){i=item[0];j=item[1];ps=series[i].datapoints.pointsize;return{datapoint:series[i].datapoints.points.slice(j*ps,(j+1)*ps),dataIndex:j,series:series[i],seriesIndex:i}}return null}function onMouseMove(e){if(options.grid.hoverable)triggerClickHoverEvent("plothover",e,function(s){return s["hoverable"]!=false})}function onMouseLeave(e){if(options.grid.hoverable)triggerClickHoverEvent("plothover",e,function(s){return false})}function onClick(e){triggerClickHoverEvent("plotclick",e,function(s){return s["clickable"]!=false})}function triggerClickHoverEvent(eventname,event,seriesFilter){var offset=eventHolder.offset(),canvasX=event.pageX-offset.left-plotOffset.left,canvasY=event.pageY-offset.top-plotOffset.top,pos=canvasToAxisCoords({left:canvasX,top:canvasY});pos.pageX=event.pageX;pos.pageY=event.pageY;var item=findNearbyItem(canvasX,canvasY,seriesFilter);if(item){item.pageX=parseInt(item.series.xaxis.p2c(item.datapoint[0])+offset.left+plotOffset.left,10);item.pageY=parseInt(item.series.yaxis.p2c(item.datapoint[1])+offset.top+plotOffset.top,10)}if(options.grid.autoHighlight){for(var i=0;i<highlights.length;++i){var h=highlights[i];if(h.auto==eventname&&!(item&&h.series==item.series&&h.point[0]==item.datapoint[0]&&h.point[1]==item.datapoint[1]))unhighlight(h.series,h.point)}if(item)highlight(item.series,item.datapoint,eventname)}placeholder.trigger(eventname,[pos,item])}function triggerRedrawOverlay(){var t=options.interaction.redrawOverlayInterval;if(t==-1){drawOverlay();return}if(!redrawTimeout)redrawTimeout=setTimeout(drawOverlay,t)}function drawOverlay(){redrawTimeout=null;octx.save();overlay.clear();octx.translate(plotOffset.left,plotOffset.top);var i,hi;for(i=0;i<highlights.length;++i){hi=highlights[i];if(hi.series.bars.show)drawBarHighlight(hi.series,hi.point);else drawPointHighlight(hi.series,hi.point)}octx.restore();executeHooks(hooks.drawOverlay,[octx])}function highlight(s,point,auto){if(typeof s=="number")s=series[s];if(typeof point=="number"){var ps=s.datapoints.pointsize;point=s.datapoints.points.slice(ps*point,ps*(point+1))}var i=indexOfHighlight(s,point);if(i==-1){highlights.push({series:s,point:point,auto:auto});triggerRedrawOverlay()}else if(!auto)highlights[i].auto=false}function unhighlight(s,point){if(s==null&&point==null){highlights=[];triggerRedrawOverlay();return}if(typeof s=="number")s=series[s];if(typeof point=="number"){var ps=s.datapoints.pointsize;point=s.datapoints.points.slice(ps*point,ps*(point+1))}var i=indexOfHighlight(s,point);if(i!=-1){highlights.splice(i,1);triggerRedrawOverlay()}}function indexOfHighlight(s,p){for(var i=0;i<highlights.length;++i){var h=highlights[i];if(h.series==s&&h.point[0]==p[0]&&h.point[1]==p[1])return i}return-1}function drawPointHighlight(series,point){var x=point[0],y=point[1],axisx=series.xaxis,axisy=series.yaxis,highlightColor=typeof series.highlightColor==="string"?series.highlightColor:$.color.parse(series.color).scale("a",.5).toString();if(x<axisx.min||x>axisx.max||y<axisy.min||y>axisy.max)return;var pointRadius=series.points.radius+series.points.lineWidth/2;octx.lineWidth=pointRadius;octx.strokeStyle=highlightColor;var radius=1.5*pointRadius;x=axisx.p2c(x);y=axisy.p2c(y);octx.beginPath();if(series.points.symbol=="circle")octx.arc(x,y,radius,0,2*Math.PI,false);else series.points.symbol(octx,x,y,radius,false);octx.closePath();octx.stroke()}function drawBarHighlight(series,point){var highlightColor=typeof series.highlightColor==="string"?series.highlightColor:$.color.parse(series.color).scale("a",.5).toString(),fillStyle=highlightColor,barLeft;switch(series.bars.align){case"left":barLeft=0;break;case"right":barLeft=-series.bars.barWidth;break;default:barLeft=-series.bars.barWidth/2}octx.lineWidth=series.bars.lineWidth;octx.strokeStyle=highlightColor;drawBar(point[0],point[1],point[2]||0,barLeft,barLeft+series.bars.barWidth,function(){return fillStyle},series.xaxis,series.yaxis,octx,series.bars.horizontal,series.bars.lineWidth)}function getColorOrGradient(spec,bottom,top,defaultColor){if(typeof spec=="string")return spec;else{var gradient=ctx.createLinearGradient(0,top,0,bottom);for(var i=0,l=spec.colors.length;i<l;++i){var c=spec.colors[i];if(typeof c!="string"){var co=$.color.parse(defaultColor);if(c.brightness!=null)co=co.scale("rgb",c.brightness);if(c.opacity!=null)co.a*=c.opacity;c=co.toString()}gradient.addColorStop(i/(l-1),c)}return gradient}}}$.plot=function(placeholder,data,options){var plot=new Plot($(placeholder),data,options,$.plot.plugins);return plot};$.plot.version="0.8.3";$.plot.plugins=[];$.fn.plot=function(data,options){return this.each(function(){$.plot(this,data,options)})};function floorInBase(n,base){return base*Math.floor(n/base)}})(jQuery);
\ No newline at end of file diff --git a/venv/lib/python3.9/site-packages/pympler/templates/jquery.flot.stack.min.js b/venv/lib/python3.9/site-packages/pympler/templates/jquery.flot.stack.min.js new file mode 100644 index 00000000..920764f5 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/jquery.flot.stack.min.js @@ -0,0 +1,7 @@ +/* Javascript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ +(function($){var options={series:{stack:null}};function init(plot){function findMatchingSeries(s,allseries){var res=null;for(var i=0;i<allseries.length;++i){if(s==allseries[i])break;if(allseries[i].stack==s.stack)res=allseries[i]}return res}function stackData(plot,s,datapoints){if(s.stack==null||s.stack===false)return;var other=findMatchingSeries(s,plot.getData());if(!other)return;var ps=datapoints.pointsize,points=datapoints.points,otherps=other.datapoints.pointsize,otherpoints=other.datapoints.points,newpoints=[],px,py,intery,qx,qy,bottom,withlines=s.lines.show,horizontal=s.bars.horizontal,withbottom=ps>2&&(horizontal?datapoints.format[2].x:datapoints.format[2].y),withsteps=withlines&&s.lines.steps,fromgap=true,keyOffset=horizontal?1:0,accumulateOffset=horizontal?0:1,i=0,j=0,l,m;while(true){if(i>=points.length)break;l=newpoints.length;if(points[i]==null){for(m=0;m<ps;++m)newpoints.push(points[i+m]);i+=ps}else if(j>=otherpoints.length){if(!withlines){for(m=0;m<ps;++m)newpoints.push(points[i+m])}i+=ps}else if(otherpoints[j]==null){for(m=0;m<ps;++m)newpoints.push(null);fromgap=true;j+=otherps}else{px=points[i+keyOffset];py=points[i+accumulateOffset];qx=otherpoints[j+keyOffset];qy=otherpoints[j+accumulateOffset];bottom=0;if(px==qx){for(m=0;m<ps;++m)newpoints.push(points[i+m]);newpoints[l+accumulateOffset]+=qy;bottom=qy;i+=ps;j+=otherps}else if(px>qx){if(withlines&&i>0&&points[i-ps]!=null){intery=py+(points[i-ps+accumulateOffset]-py)*(qx-px)/(points[i-ps+keyOffset]-px);newpoints.push(qx);newpoints.push(intery+qy);for(m=2;m<ps;++m)newpoints.push(points[i+m]);bottom=qy}j+=otherps}else{if(fromgap&&withlines){i+=ps;continue}for(m=0;m<ps;++m)newpoints.push(points[i+m]);if(withlines&&j>0&&otherpoints[j-otherps]!=null)bottom=qy+(otherpoints[j-otherps+accumulateOffset]-qy)*(px-qx)/(otherpoints[j-otherps+keyOffset]-qx);newpoints[l+accumulateOffset]+=bottom;i+=ps}fromgap=false;if(l!=newpoints.length&&withbottom)newpoints[l+2]+=bottom}if(withsteps&&l!=newpoints.length&&l>0&&newpoints[l]!=null&&newpoints[l]!=newpoints[l-ps]&&newpoints[l+1]!=newpoints[l-ps+1]){for(m=0;m<ps;++m)newpoints[l+ps+m]=newpoints[l+m];newpoints[l+1]=newpoints[l-ps+1]}}datapoints.points=newpoints}plot.hooks.processDatapoints.push(stackData)}$.plot.plugins.push({init:init,options:options,name:"stack",version:"1.2"})})(jQuery);
\ No newline at end of file diff --git a/venv/lib/python3.9/site-packages/pympler/templates/jquery.flot.tooltip.min.js b/venv/lib/python3.9/site-packages/pympler/templates/jquery.flot.tooltip.min.js new file mode 100644 index 00000000..8e98bdc5 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/jquery.flot.tooltip.min.js @@ -0,0 +1,12 @@ +/* + * jquery.flot.tooltip + * + * description: easy-to-use tooltips for Flot charts + * version: 0.8.4 + * authors: Krzysztof Urbas @krzysu [myviews.pl],Evan Steinkerchner @Roundaround + * website: https://github.com/krzysu/flot.tooltip + * + * build on 2014-08-06 + * released under MIT License, 2012 +*/ +!function(a){var b={tooltip:!1,tooltipOpts:{id:"flotTip",content:"%s | X: %x | Y: %y",xDateFormat:null,yDateFormat:null,monthNames:null,dayNames:null,shifts:{x:10,y:20},defaultTheme:!0,lines:!1,onHover:function(){},$compat:!1}},c=function(a){this.tipPosition={x:0,y:0},this.init(a)};c.prototype.init=function(b){function c(a){var c={};c.x=a.pageX,c.y=a.pageY,b.setTooltipPosition(c)}function d(c,d,f){var g=function(a,b,c,d){return Math.sqrt((c-a)*(c-a)+(d-b)*(d-b))},h=function(a,b,c,d,e,f,h){if(!h||(h=function(a,b,c,d,e,f){if("undefined"!=typeof c)return{x:c,y:b};if("undefined"!=typeof d)return{x:a,y:d};var g,h=-1/((f-d)/(e-c));return{x:g=(e*(a*h-b+d)+c*(a*-h+b-f))/(h*(e-c)+d-f),y:h*g-h*a+b}}(a,b,c,d,e,f),h.x>=Math.min(c,e)&&h.x<=Math.max(c,e)&&h.y>=Math.min(d,f)&&h.y<=Math.max(d,f))){var i=d-f,j=e-c,k=c*f-d*e;return Math.abs(i*a+j*b+k)/Math.sqrt(i*i+j*j)}var l=g(a,b,c,d),m=g(a,b,e,f);return l>m?m:l};if(f)b.showTooltip(f,d);else if(e.plotOptions.series.lines.show&&e.tooltipOptions.lines===!0){var i=e.plotOptions.grid.mouseActiveRadius,j={distance:i+1};a.each(b.getData(),function(a,c){for(var e=0,f=-1,i=1;i<c.data.length;i++)c.data[i-1][0]<=d.x&&c.data[i][0]>=d.x&&(e=i-1,f=i);if(-1===f)return void b.hideTooltip();var k={x:c.data[e][0],y:c.data[e][1]},l={x:c.data[f][0],y:c.data[f][1]},m=h(c.xaxis.p2c(d.x),c.yaxis.p2c(d.y),c.xaxis.p2c(k.x),c.yaxis.p2c(k.y),c.xaxis.p2c(l.x),c.yaxis.p2c(l.y),!1);if(m<j.distance){var n=g(k.x,k.y,d.x,d.y)<g(d.x,d.y,l.x,l.y)?e:f,o=(c.datapoints.pointsize,[d.x,k.y+(l.y-k.y)*((d.x-k.x)/(l.x-k.x))]),p={datapoint:o,dataIndex:n,series:c,seriesIndex:a};j={distance:m,item:p}}}),j.distance<i+1?b.showTooltip(j.item,d):b.hideTooltip()}else b.hideTooltip()}var e=this,f=a.plot.plugins.length;if(this.plotPlugins=[],f)for(var g=0;f>g;g++)this.plotPlugins.push(a.plot.plugins[g].name);b.hooks.bindEvents.push(function(b,f){if(e.plotOptions=b.getOptions(),e.plotOptions.tooltip!==!1&&"undefined"!=typeof e.plotOptions.tooltip){e.tooltipOptions=e.plotOptions.tooltipOpts,e.tooltipOptions.$compat?(e.wfunc="width",e.hfunc="height"):(e.wfunc="innerWidth",e.hfunc="innerHeight");{e.getDomElement()}a(b.getPlaceholder()).bind("plothover",d),a(f).bind("mousemove",c)}}),b.hooks.shutdown.push(function(b,e){a(b.getPlaceholder()).unbind("plothover",d),a(e).unbind("mousemove",c)}),b.setTooltipPosition=function(b){var c=e.getDomElement(),d=c.outerWidth()+e.tooltipOptions.shifts.x,f=c.outerHeight()+e.tooltipOptions.shifts.y;b.x-a(window).scrollLeft()>a(window)[e.wfunc]()-d&&(b.x-=d),b.y-a(window).scrollTop()>a(window)[e.hfunc]()-f&&(b.y-=f),e.tipPosition.x=b.x,e.tipPosition.y=b.y},b.showTooltip=function(a,c){var d=e.getDomElement(),f=e.stringFormat(e.tooltipOptions.content,a);d.html(f),b.setTooltipPosition({x:c.pageX,y:c.pageY}),d.css({left:e.tipPosition.x+e.tooltipOptions.shifts.x,top:e.tipPosition.y+e.tooltipOptions.shifts.y}).show(),"function"==typeof e.tooltipOptions.onHover&&e.tooltipOptions.onHover(a,d)},b.hideTooltip=function(){e.getDomElement().hide().html("")}},c.prototype.getDomElement=function(){var b=a("#"+this.tooltipOptions.id);return 0===b.length&&(b=a("<div />").attr("id",this.tooltipOptions.id),b.appendTo("body").hide().css({position:"absolute"}),this.tooltipOptions.defaultTheme&&b.css({background:"#fff","z-index":"1040",padding:"0.4em 0.6em","border-radius":"0.5em","font-size":"0.8em",border:"1px solid #111",display:"none","white-space":"nowrap"})),b},c.prototype.stringFormat=function(a,b){var c,d,e,f,g=/%p\.{0,1}(\d{0,})/,h=/%s/,i=/%lx/,j=/%ly/,k=/%x\.{0,1}(\d{0,})/,l=/%y\.{0,1}(\d{0,})/,m="%x",n="%y",o="%ct";if("undefined"!=typeof b.series.threshold?(c=b.datapoint[0],d=b.datapoint[1],e=b.datapoint[2]):"undefined"!=typeof b.series.lines&&b.series.lines.steps?(c=b.series.datapoints.points[2*b.dataIndex],d=b.series.datapoints.points[2*b.dataIndex+1],e=""):(c=b.series.data[b.dataIndex][0],d=b.series.data[b.dataIndex][1],e=b.series.data[b.dataIndex][2]),null===b.series.label&&b.series.originSeries&&(b.series.label=b.series.originSeries.label),"function"==typeof a&&(a=a(b.series.label,c,d,b)),"undefined"!=typeof b.series.percent?f=b.series.percent:"undefined"!=typeof b.series.percents&&(f=b.series.percents[b.dataIndex]),"number"==typeof f&&(a=this.adjustValPrecision(g,a,f)),a="undefined"!=typeof b.series.label?a.replace(h,b.series.label):a.replace(h,""),a=this.hasAxisLabel("xaxis",b)?a.replace(i,b.series.xaxis.options.axisLabel):a.replace(i,""),a=this.hasAxisLabel("yaxis",b)?a.replace(j,b.series.yaxis.options.axisLabel):a.replace(j,""),this.isTimeMode("xaxis",b)&&this.isXDateFormat(b)&&(a=a.replace(k,this.timestampToDate(c,this.tooltipOptions.xDateFormat,b.series.xaxis.options))),this.isTimeMode("yaxis",b)&&this.isYDateFormat(b)&&(a=a.replace(l,this.timestampToDate(d,this.tooltipOptions.yDateFormat,b.series.yaxis.options))),"number"==typeof c&&(a=this.adjustValPrecision(k,a,c)),"number"==typeof d&&(a=this.adjustValPrecision(l,a,d)),"undefined"!=typeof b.series.xaxis.ticks){var p;p=this.hasRotatedXAxisTicks(b)?"rotatedTicks":"ticks";var q=b.dataIndex+b.seriesIndex;if(b.series.xaxis[p].length>q&&!this.isTimeMode("xaxis",b)){var r=this.isCategoriesMode("xaxis",b)?b.series.xaxis[p][q].label:b.series.xaxis[p][q].v;r===c&&(a=a.replace(k,b.series.xaxis[p][q].label))}}if("undefined"!=typeof b.series.yaxis.ticks)for(var s in b.series.yaxis.ticks)if(b.series.yaxis.ticks.hasOwnProperty(s)){var t=this.isCategoriesMode("yaxis",b)?b.series.yaxis.ticks[s].label:b.series.yaxis.ticks[s].v;t===d&&(a=a.replace(l,b.series.yaxis.ticks[s].label))}return"undefined"!=typeof b.series.xaxis.tickFormatter&&(a=a.replace(m,b.series.xaxis.tickFormatter(c,b.series.xaxis).replace(/\$/g,"$$"))),"undefined"!=typeof b.series.yaxis.tickFormatter&&(a=a.replace(n,b.series.yaxis.tickFormatter(d,b.series.yaxis).replace(/\$/g,"$$"))),e&&(a=a.replace(o,e)),a},c.prototype.isTimeMode=function(a,b){return"undefined"!=typeof b.series[a].options.mode&&"time"===b.series[a].options.mode},c.prototype.isXDateFormat=function(){return"undefined"!=typeof this.tooltipOptions.xDateFormat&&null!==this.tooltipOptions.xDateFormat},c.prototype.isYDateFormat=function(){return"undefined"!=typeof this.tooltipOptions.yDateFormat&&null!==this.tooltipOptions.yDateFormat},c.prototype.isCategoriesMode=function(a,b){return"undefined"!=typeof b.series[a].options.mode&&"categories"===b.series[a].options.mode},c.prototype.timestampToDate=function(b,c,d){var e=a.plot.dateGenerator(b,d);return a.plot.formatDate(e,c,this.tooltipOptions.monthNames,this.tooltipOptions.dayNames)},c.prototype.adjustValPrecision=function(a,b,c){var d,e=b.match(a);return null!==e&&""!==RegExp.$1&&(d=RegExp.$1,c=c.toFixed(d),b=b.replace(a,c)),b},c.prototype.hasAxisLabel=function(b,c){return-1!==a.inArray(this.plotPlugins,"axisLabels")&&"undefined"!=typeof c.series[b].options.axisLabel&&c.series[b].options.axisLabel.length>0},c.prototype.hasRotatedXAxisTicks=function(b){return-1!==a.inArray(this.plotPlugins,"tickRotor")&&"undefined"!=typeof b.series.xaxis.rotatedTicks};var d=function(a){new c(a)};a.plot.plugins.push({init:d,options:b,name:"tooltip",version:"0.8.4"})}(jQuery);
\ No newline at end of file diff --git a/venv/lib/python3.9/site-packages/pympler/templates/memory_panel.html b/venv/lib/python3.9/site-packages/pympler/templates/memory_panel.html new file mode 100644 index 00000000..f68cc379 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/memory_panel.html @@ -0,0 +1,58 @@ +{% load i18n %}{% load static %} +<script type="text/javascript" src="{% static 'jquery.sparkline.min.js' %}"></script> +<table> + <colgroup> + <col style="width:20%"/> + <col/> + </colgroup> + <thead> + <tr> + <th>{% trans "Resource" %}</th> + <th>{% trans "Value" %}</th> + </tr> + </thead> + <tbody> + {% for key, value in rows %} + <tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}"> + <td>{{ key|escape }}</td> + <td>{{ value|escape }}</td> + </tr> + {% endfor %} + </tbody> +</table> + +<table> + <colgroup> + <col style="width:20%"/> + <col/> + <col/> + </colgroup> + <thead> + <tr> + <th>{% trans "Class" %}</th> + <th>{% trans "Number of instances" %} <a class="show_sparkline" href="#">Show sparklines</a></th> + <th>{% trans "Total size" %}</th> + </tr> + </thead> + <tbody> + {% for cls, history, size in classes %} + <tr class="{% cycle 'djDebugOdd' 'djDebugEven' %}"> + <td>{{ cls|escape }}</td> + <td id="{{ cls|escape|cut:'.' }}_history" values="{{ history|join:',' }}"> + {{ history|safeseq|join:', ' }} + </td> + <td>{{ size|escape }}</td> + </tr> + {% endfor %} + </tbody> +</table> +<script type="text/javascript"> + (function ($) { + window.jQuery = $; // for jquery.sparkline + $("#MemoryPanel .show_sparkline").on('click', function() { + {% for cls, _, _ in classes %} + $("#{{ cls|escape|cut:'.' }}_history").sparkline('html', {width: '200px'}); + {% endfor %} + }); + })(jQuery || djdt.jQuery); +</script> diff --git a/venv/lib/python3.9/site-packages/pympler/templates/process.tpl b/venv/lib/python3.9/site-packages/pympler/templates/process.tpl new file mode 100644 index 00000000..3f5d54ff --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/process.tpl @@ -0,0 +1,89 @@ +%include('header', category='Process', title='Process Information') +%from pympler.util.stringutils import pp + +<h1>Process information</h1> + +<table class="tdata"> + <tbody> + <tr> + <th>Virtual size:</th> + <td class="num">{{pp(info.vsz)}}</td> + </tr> + <tr> + <th>Physical memory size:</th> + <td class="num">{{pp(info.rss)}}</td> + </tr> + <tr> + <th>Major pagefaults:</th> + <td class="num">{{info.pagefaults}}</td> + </tr> + %for key, value in info.os_specific: + <tr> + <th>{{key}}:</th> + <td class="num">{{value}}</td> + </tr> + %end + </tbody> +</table> + +<h2>Thread information</h2> + +<table class="tdata"> + <tbody> + <tr> + <th>ID</th> + <th>Name</th> + <th>Daemon</th> + </tr> + %for tinfo in threads: + <tr> + <td>{{tinfo.ident}}</td> + <td>{{tinfo.name}}</td> + <td>{{tinfo.daemon}}</td> + </tr> + %end + </tbody> +</table> + +<h2>Thread stacks</h2> + +<div class="stacks"> + %for tinfo in threads: + <div class="stacktrace" id="{{tinfo.ident}}"> + <a class="show_traceback" href="#">Traceback for thread {{tinfo.name}}</a> + </div> + %end +</div> + +<script type="text/javascript"> + $(".show_traceback").click(function() { + var tid = $(this).parent().attr("id"); + $.get("/traceback/"+tid, function(data) { + $("#"+tid).replaceWith(data); + }); + return false; + }); + $(".stacks").delegate(".expand_local", "click", function() { + var oid = $(this).attr("id"); + $.get("/objects/"+oid, function(data) { + $("#"+oid).replaceWith(data); + }); + return false; + }); + $(".stacks").delegate(".expand_ref", "click", function() { + var node_id = $(this).attr("id"); + var oid = node_id.split("_")[0]; + $.get("/objects/"+oid, function(data) { + $("#children_"+node_id).append(data); + }); + $(this).removeClass("expand_ref").addClass("toggle_ref"); + return false; + }); + $(".stacks").delegate(".toggle_ref", "click", function() { + var node_id = $(this).attr("id"); + $("#children_"+node_id).toggle(); + return false; + }); +</script> + +%include('footer') diff --git a/venv/lib/python3.9/site-packages/pympler/templates/referents.tpl b/venv/lib/python3.9/site-packages/pympler/templates/referents.tpl new file mode 100644 index 00000000..6a880622 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/referents.tpl @@ -0,0 +1,13 @@ +%from random import randint +%for name, (ref, type_name, obj_repr, size) in referents.items(): + %ref = "%s_%s" % (ref, randint(0, 65535)) + <div class="referents"> + <a class="expand_ref" id="{{ref}}" href="#"> + <span class="local_name">{{name}}</span> + <span class="local_type">{{type_name}}</span> + <span class="local_size">{{size}}</span> + <span class="local_value">{{obj_repr}}</span> + </a> + <span id="children_{{ref}}"/> + </div> +%end diff --git a/venv/lib/python3.9/site-packages/pympler/templates/stacktrace.tpl b/venv/lib/python3.9/site-packages/pympler/templates/stacktrace.tpl new file mode 100644 index 00000000..c56588dc --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/stacktrace.tpl @@ -0,0 +1,30 @@ +<div class="stacktrace">
+ <strong>Stacktrace for thread {{threadid}}</strong>
+ %for frame in stack:
+ <div class="stackframe">
+ <span class="filename">{{frame[1]}}</span>
+ <span class="lineno">{{frame[2]}}</span>
+ <span class="function">{{frame[3]}}</span>
+ %if frame[4]:
+ %context = frame[4]
+ <div class="context">
+ %highlight = len(context) / 2
+ %for idx, line in enumerate(frame[4]):
+ %hl = (idx == highlight) and "highlighted" or ""
+ %if line.strip():
+ <div class="{{hl}}" style="padding-left:{{len(line)-len(line.lstrip())}}em" width="100%">
+ {{line.strip()}}
+ </div>
+ %end
+ %end
+ </div>
+ %end
+ <div class="local">
+ <a class="expand_local" id="{{frame[0]}}" href="#">Show locals</a>
+ </div>
+ </div>
+ %end
+ %if not stack:
+ Cannot retrieve stacktrace for thread {{threadid}}.
+ %end
+</div>
diff --git a/venv/lib/python3.9/site-packages/pympler/templates/style.css b/venv/lib/python3.9/site-packages/pympler/templates/style.css new file mode 100644 index 00000000..d7fcc6a5 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/style.css @@ -0,0 +1,992 @@ +/** + * Sphinx Doc Design + */ + +body { + font-family: "Verdana", "Tahoma", Sans-Serif; + font-size: 90%; + background-color: #11303d; + color: #000; + margin: 0; + padding: 0; +} + +/* :::: LAYOUT :::: */ + +div.document { + background-color: #158906; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 0; +} + +div.body { + background-color: white; + padding: 0 20px 16px 20px; +} + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + + +div.sphinxsidebar { + display: none; +} + +div.sphinxsidebar { + float: right; + width: 230px; + margin-left: -100%; + font-size: 90%; +} + +div.clearer { + clear: both; +} + +div.footer { + color: #fff; + width: 100%; + padding: 9px 0 9px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #fff; + text-decoration: underline; +} + +div.related { + background-color: #5A3D31; + color: #fff; + width: 100%; + height: 30px; + line-height: 30px; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +div.related a { + color: white; +} + +/* ::: TOC :::: */ +div.sphinxsidebar h3 { + color: white; + font-size: 1.4em; + font-weight: normal; + margin: 0; + padding: 0; +} + +div.sphinxsidebar h4 { + color: white; + font-size: 1.3em; + font-weight: normal; + margin: 5px 0 0 0; + padding: 0; +} + +div.sphinxsidebar p { + color: white; +} + +div.sphinxsidebar p.topless { + margin: 5px 10px 10px 10px; +} + +div.sphinxsidebar ul { + margin: 10px; + padding: 0; + list-style: none; + color: white; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar a { + color: #A4FF98; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #A4FF98; + font-size: 1em; +} + +/* :::: MODULE CLOUD :::: */ +div.modulecloud { + margin: -5px 10px 5px 10px; + padding: 10px; + line-height: 160%; + border: 1px solid #cbe7e5; + background-color: #f2fbfd; +} + +div.modulecloud a { + padding: 0 5px 0 5px; +} + +/* :::: SEARCH :::: */ +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* :::: COMMON FORM STYLES :::: */ + +div.actions { + padding: 5px 10px 5px 10px; + border-top: 1px solid #cbe7e5; + border-bottom: 1px solid #cbe7e5; + background-color: #e0f6f4; +} + +form dl { + color: #333; +} + +form dt { + clear: both; + float: left; + min-width: 110px; + margin-right: 10px; + padding-top: 2px; +} + +input#homepage { + display: none; +} + +div.error { + margin: 5px 20px 0 0; + padding: 5px; + border: 1px solid #d00; + font-weight: bold; +} + +/* :::: INLINE COMMENTS :::: */ + +div.inlinecomments { + position: absolute; + right: 20px; +} + +div.inlinecomments a.bubble { + display: block; + float: right; + background-image: url(style/comment.png); + background-repeat: no-repeat; + width: 25px; + height: 25px; + text-align: center; + padding-top: 3px; + font-size: 0.9em; + line-height: 14px; + font-weight: bold; + color: black; +} + +div.inlinecomments a.bubble span { + display: none; +} + +div.inlinecomments a.emptybubble { + background-image: url(style/nocomment.png); +} + +div.inlinecomments a.bubble:hover { + background-image: url(style/hovercomment.png); + text-decoration: none; + color: #3ca0a4; +} + +div.inlinecomments div.comments { + float: right; + margin: 25px 5px 0 0; + max-width: 50em; + min-width: 30em; + border: 1px solid #2eabb0; + background-color: #f2fbfd; + z-index: 150; +} + +div#comments { + border: 1px solid #2eabb0; + margin-top: 20px; +} + +div#comments div.nocomments { + padding: 10px; + font-weight: bold; +} + +div.inlinecomments div.comments h3, +div#comments h3 { + margin: 0; + padding: 0; + background-color: #2eabb0; + color: white; + border: none; + padding: 3px; +} + +div.inlinecomments div.comments div.actions { + padding: 4px; + margin: 0; + border-top: none; +} + +div#comments div.comment { + margin: 10px; + border: 1px solid #2eabb0; +} + +div.inlinecomments div.comment h4, +div.commentwindow div.comment h4, +div#comments div.comment h4 { + margin: 10px 0 0 0; + background-color: #2eabb0; + color: white; + border: none; + padding: 1px 4px 1px 4px; +} + +div#comments div.comment h4 { + margin: 0; +} + +div#comments div.comment h4 a { + color: #d5f4f4; +} + +div.inlinecomments div.comment div.text, +div.commentwindow div.comment div.text, +div#comments div.comment div.text { + margin: -5px 0 -5px 0; + padding: 0 10px 0 10px; +} + +div.inlinecomments div.comment div.meta, +div.commentwindow div.comment div.meta, +div#comments div.comment div.meta { + text-align: right; + padding: 2px 10px 2px 0; + font-size: 95%; + color: #538893; + border-top: 1px solid #cbe7e5; + background-color: #e0f6f4; +} + +div.commentwindow { + position: absolute; + width: 500px; + border: 1px solid #cbe7e5; + background-color: #f2fbfd; + display: none; + z-index: 130; +} + +div.commentwindow h3 { + margin: 0; + background-color: #2eabb0; + color: white; + border: none; + padding: 5px; + font-size: 1.5em; + cursor: pointer; +} + +div.commentwindow div.actions { + margin: 10px -10px 0 -10px; + padding: 4px 10px 4px 10px; + color: #538893; +} + +div.commentwindow div.actions input { + border: 1px solid #2eabb0; + background-color: white; + color: #135355; + cursor: pointer; +} + +div.commentwindow div.form { + padding: 0 10px 0 10px; +} + +div.commentwindow div.form input, +div.commentwindow div.form textarea { + border: 1px solid #3c9ea2; + background-color: white; + color: black; +} + +div.commentwindow div.error { + margin: 10px 5px 10px 5px; + background-color: #fbe5dc; + display: none; +} + +div.commentwindow div.form textarea { + width: 99%; +} + +div.commentwindow div.preview { + margin: 10px 0 10px 0; + background-color: #70d0d4; + padding: 0 1px 1px 25px; +} + +div.commentwindow div.preview h4 { + margin: 0 0 -5px -20px; + padding: 4px 0 0 4px; + color: white; + font-size: 1.3em; +} + +div.commentwindow div.preview div.comment { + background-color: #f2fbfd; +} + +div.commentwindow div.preview div.comment h4 { + margin: 10px 0 0 0!important; + padding: 1px 4px 1px 4px!important; + font-size: 1.2em; +} + +/* :::: SUGGEST CHANGES :::: */ +div#suggest-changes-box input, div#suggest-changes-box textarea { + border: 1px solid #ccc; + background-color: white; + color: black; +} + +div#suggest-changes-box textarea { + width: 99%; + height: 400px; +} + + +/* :::: PREVIEW :::: */ +div.preview { + background-image: url(style/preview.png); + padding: 0 20px 20px 20px; + margin-bottom: 30px; +} + + +/* :::: INDEX PAGE :::: */ + +table.contentstable { + width: 90%; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* :::: INDEX STYLES :::: */ + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable dl, table.indextable dd { + margin-top: 0; + margin-bottom: 0; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +form.pfform { + margin: 10px 0 20px 0; +} + +/* :::: GLOBAL STYLES :::: */ + +.docwarning { + background-color: #ffe4e4; + padding: 10px; + margin: 0 -20px 0 -20px; + border-bottom: 1px solid #f66; +} + +p.subhead { + font-weight: bold; + margin-top: 20px; +} + +a { + color: #355F7C; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-weight: bold; + margin: 16px -20px 16px -20px; + padding: 3px 0 3px 10px; +} + +div.body h1 { + margin-top: 0; + font-size: 140%; + background-color: #E5EDB8; + color: #000; + border-bottom: 1px solid #ccc; +} +div.body h2 { + font-size: 120%; + background-color: #E5EDB8; + color: #000; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} +div.body h3 { + font-size: 120%; + border-bottom: 1px dashed #ccc; +} +div.body h4 { font-size: 120%; } +div.body h5 { font-size: 110%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink { + visibility: visible; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + line-height: 100%; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +ul.fakelist { + list-style: none; + margin: 10px 0 10px 20px; + padding: 0; +} + +.field-list ul { + padding-left: 1em; +} + +.first { + margin-top: 0 !important; +} + +/* "Footnotes" heading */ +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +/* "Topics" */ + +div.topic { + background-color: #eee; + border: 1px solid #ccc; + padding: 0 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* Admonitions */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +div.admonition p { + display: inline; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +table.docutils { + border: 0; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 0; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.field-list td, table.field-list th { + border: 0 !important; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +dl { + margin-bottom: 1px; + clear: both; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.refcount { + color: #060; +} + +dt:target, +.highlight { + background-color: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +th { + text-align: left; + padding-right: 5px; +} + +pre { + padding: 5px; + background-color: #efc; + color: #333; + border: 1px solid #ac9; + border-left: none; + border-right: none; + overflow: auto; +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +tt { + background-color: #ecf0f3; + padding: 0 1px 0 1px; + font-size: 0.95em; +} + +tt.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +tt.descclassname { + background-color: transparent; +} + +tt.xref, a tt { + background-color: transparent; + font-weight: bold; +} + +.footnote:target { background-color: #ffa } + +h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.versionmodified { + font-style: italic; +} + +form.comment { + margin: 0; + padding: 10px 30px 10px 30px; + background-color: #eee; +} + +form.comment h3 { + background-color: #326591; + color: white; + margin: -10px -30px 10px -30px; + padding: 5px; + font-size: 1.4em; +} + +form.comment input, +form.comment textarea { + border: 1px solid #ccc; + padding: 2px; + font-size: 100%; +} + +form.comment input[type="text"] { + width: 240px; +} + +form.comment textarea { + width: 100%; + height: 200px; + margin-bottom: 10px; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +/* :::: PRINT :::: */ +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0; + width : 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + div#comments div.new-comment-box, + #top-link { + display: none; + } +} + +/* :::: OWN :::: */ + +.section ul { + margin: 0.2em; +} + +/* Data tables */ + +table.tdata +{ + font-size: 12px; + background: #fff; + border-collapse: collapse; + text-align: left; +} +table.tdata th +{ + font-size: 14px; + font-weight: normal; + padding: 8px 8px; + background-color: #E5EDB8; + border-bottom: 1px solid #ccc; +} + +table.tdata tr:first-child th:first-child +{ + border-top-left-radius: 8px; +} + +table.tdata tr:first-child th:last-child +{ + border-top-right-radius: 8px; +} + +table.tdata td +{ + border-bottom: 1px solid #ccc; + padding: 8px 8px; +} + +table.tdata tbody tr:hover td +{ + color: #060; +} + +table th.num, +table td.num +{ + text-align: right; +} + +div.stacktrace +{ + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + border-radius: 6px; + border: 1px solid #999; + padding: 6px; + margin-bottom: 1em; +} + +div.stacktrace div.stackframe +{ + margin: 8px; +} + +div.stackframe .context, +div.stackframe .local +{ + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + padding: 4px; + margin-left: 10px; + margin-top: 4px; + background: #f2f2f2; +} + +div.stackframe div.context div.highlighted +{ + background: #E5EDB8; +} + +div.stackframe span.lineno, +div.stackframe span.filename +{ + +} + +div.stackframe span.function +{ + font-weight: bold; +} + +div.stackframe span.function, +div.stackframe div.context +{ + font-family: monospace; +} + +div.local, +div.referents +{ + margin-top: 4px; + margin-bottom: 4px; + margin-left: 10px; +} + +div.stackframe div.local +{ + background: #fafafa; +} + +div.referents span.local_name, +div.referents span.local_size +{ + font-family: monospace; + padding: 1px; +} + +div.referents span.local_value, +div.referents span.local_size, +div.referents span.local_type +{ + font-family: monospace; + margin-left: 1em; +} +div.referents span.local_type +{ + color: #999; +} +div.referents span.local_size +{ + color: #03F; +} +div.local div.referents a +{ + color: #000; + text-decoration: none; +} +div.local div.referents a:hover +{ + background-color: #EEE; + color: #090; + text-decoration: none; +} + diff --git a/venv/lib/python3.9/site-packages/pympler/templates/tracker.tpl b/venv/lib/python3.9/site-packages/pympler/templates/tracker.tpl new file mode 100644 index 00000000..8533e9eb --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/tracker.tpl @@ -0,0 +1,127 @@ +%include('header', category='Tracker', title='Tracked objects') + +%from pympler.util.stringutils import pp, pp_timestamp +%from json import dumps + +<h1>Tracked objects</h1> + +%if snapshots: + + <h2>Memory distribution over time</h2> + + <div id="memory_chart_flot" style="width:100%; height: 400px"></div> + + <script type="text/javascript" src="/static/jquery.flot.min.js"></script> + <script type="text/javascript" src="/static/jquery.flot.stack.min.js"></script> + <script type="text/javascript" src="/static/jquery.flot.tooltip.min.js"></script> + <script type="text/javascript"> + function format_size(value) { + var val = Math.round(value / (1000*1000)); + return val.toLocaleString() + ' MB'; + }; + + $(document).ready(function() { + var timeseries = {{!timeseries}}; + var options = { + xaxis: { + show: false, + }, + yaxis: { + tickFormatter: format_size + }, + grid: { + hoverable: true + }, + tooltip: true, + tooltipOpts: { + content: "%s | %y" + }, + legend: { + position: "nw" + }, + series: { + bars: { + show: true, + barWidth: .9, + fillColor: { colors: [ { opacity: 0.9 }, { opacity: 0.9 } ] }, + align: "center" + }, + stack: true + } + }; + $.plot($('#memory_chart_flot'), timeseries, options); + }); + </script> + + <h2>Snapshots statistics</h2> + + %for sn in snapshots: + <h3>{{sn.desc or 'Untitled'}} snapshot at {{pp_timestamp(sn.timestamp)}}</h3> + <table class="tdata"> + <thead> + <tr> + <th width="20%">Class</th> + <th width="20%" class="num">Instance #</th> + <th width="20%" class="num">Total</th> + <th width="20%" class="num">Average size</th> + <th width="20%" class="num">Share</th> + </tr> + </thead> + <tbody> + %cnames = list(sn.classes.keys()) + %cnames.sort() + %for cn in cnames: + %data = sn.classes[cn] + <tr> + <td><a href="/tracker/class/{{cn}}">{{cn}}</a></td> + <td class="num">{{data['active']}}</td> + <td class="num">{{pp(data['sum'])}}</td> + <td class="num">{{pp(data['avg'])}}</td> + <td class="num">{{'%3.2f%%' % data['pct']}}</td> + </tr> + %end + </tbody> + </table> + + %if sn.system_total.available: + <h4>Process memory</h4> + + <table class="tdata"> + <thead> + <tr> + <th>Type</th> + <th class="num">Size</th> + </tr> + </thead> + <tbody> + <tr> + <td>Virtual memory size</td> + <td class="num">{{pp(sn.system_total.vsz)}}</td> + </tr> + <tr> + <td>Resident set size</td> + <td class="num">{{pp(sn.system_total.rss)}}</td> + </tr> + <tr> + <td>Pagefaults</td> + <td class="num">{{sn.system_total.pagefaults}}</td> + </tr> + %for key, value in sn.system_total.os_specific: + <tr> + <td>{{key}}</td> + <td class="num">{{value}}</td> + </tr> + %end + </tbody> + </table> + %end + %end + +%else: + + <p>No objects are currently tracked. Consult the Pympler documentation for + instructions of how to use the classtracker module.</p> + +%end + +%include('footer') diff --git a/venv/lib/python3.9/site-packages/pympler/templates/tracker_class.tpl b/venv/lib/python3.9/site-packages/pympler/templates/tracker_class.tpl new file mode 100644 index 00000000..047e730a --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/templates/tracker_class.tpl @@ -0,0 +1,79 @@ +%include('header', category='Tracker', title=clsname) + +%from pympler.util.stringutils import pp, pp_timestamp + +<h1>{{clsname}}</h1> + +%sizes = [tobj.get_max_size() for tobj in stats.index[clsname]] + +<p>{{len(stats.index[clsname])}} instances of {{clsname}} were registered. The +average size is {{pp(sum(sizes)/len(sizes))}}, the minimal size is +{{pp(min(sizes))}}, the maximum size is {{pp(max(sizes))}}.</p> + +<h2>Coalesced Referents per Snapshot</h2> + +%for snapshot in stats.snapshots: + %if clsname in snapshot.classes: + %merged = snapshot.classes[clsname]['merged'] + <h3>Snapshot: {{snapshot.desc}}</h3> + <p>{{pp(merged.size)}} occupied by instances of class {{clsname}}</p> + %if merged.refs: + %include('asized_referents', referents=merged.refs) + %else: + <p>No per-referent sizes recorded.</p> + %end + %end +%end + +<h2>Instances</h2> + +%for tobj in stats.index[clsname]: + <table class="tdata" width="100%" rules="rows"> + <tr> + <th width="140px">Instance</th> + <td>{{tobj.name}} at {{'0x%08x' % tobj.id}}</td> + </tr> + %if tobj.repr: + <tr> + <th>Representation</th> + <td>{{tobj.repr}} </td> + </tr> + %end + <tr> + <th>Lifetime</th> + <td>{{pp_timestamp(tobj.birth)}} - {{pp_timestamp(tobj.death)}}</td> + </tr> + %if getattr(tobj, 'trace'): + <tr> + <th>Instantiation</th> + <td> + % # <div class="stacktrace"> + %for frame in tobj.trace: + <div class="stackframe"> + <span class="filename">{{frame[0]}}</span> + <span class="lineno">{{frame[1]}}</span> + <span class="function">{{frame[2]}}</span> + <div class="context">{{frame[3][0].strip()}}</div> + </div> + %end + % # </div> + </td> + </tr> + %end + %for (timestamp, size) in tobj.snapshots: + <tr> + <td>{{pp_timestamp(timestamp)}}</td> + %if not size.refs: + <td>{{pp(size.size)}}</td> + %else: + <td> + {{pp(size.size)}} + %include('asized_referents', referents=size.refs) + </td> + %end + </tr> + %end + </table> +%end + +%include('footer') diff --git a/venv/lib/python3.9/site-packages/pympler/tracker.py b/venv/lib/python3.9/site-packages/pympler/tracker.py new file mode 100644 index 00000000..ec88e116 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/tracker.py @@ -0,0 +1,267 @@ +"""The tracker module allows you to track changes in the memory usage over +time. + +Using the SummaryTracker, you can create summaries and compare them +with each other. Stored summaries can be ignored during comparison, +avoiding the observer effect. + +The ObjectTracker allows to monitor object creation. You create objects from +one time and compare with objects from an earlier time. + +""" +import gc +import inspect + +from pympler import muppy, summary +from pympler.util import compat + + +class SummaryTracker(object): + """ Helper class to track changes between two summaries taken. + + Detailed information on single objects will be lost, e.g. object size or + object id. But often summaries are sufficient to monitor the memory usage + over the lifetime of an application. + + On initialisation, a first summary is taken. Every time `diff` is called, + a new summary will be created. Thus, a diff between the new and the last + summary can be extracted. + + Be aware that filtering out previous summaries is time-intensive. You + should therefore restrict yourself to the number of summaries you really + need. + + """ + def __init__(self, ignore_self=True): + """Constructor. + + The number of summaries managed by the tracker has a performance + impact on new summaries, iff you decide to exclude them from further + summaries. Therefore it is suggested to use them economically. + + Keyword arguments: + ignore_self -- summaries managed by this object will be ignored. + """ + self.s0 = summary.summarize(muppy.get_objects()) + self.summaries = {} + self.ignore_self = ignore_self + + def create_summary(self): + """Return a summary. + + See also the notes on ignore_self in the class as well as the + initializer documentation. + + """ + if not self.ignore_self: + res = summary.summarize(muppy.get_objects()) + else: + # If the user requested the data required to store summaries to be + # ignored in the summaries, we need to identify all objects which + # are related to each summary stored. + # Thus we build a list of all objects used for summary storage as + # well as a dictionary which tells us how often an object is + # referenced by the summaries. + # During this identification process, more objects are referenced, + # namely int objects identifying referenced objects as well as the + # corresponding count. + # For all these objects it will be checked whether they are + # referenced from outside the monitor's scope. If not, they will be + # subtracted from the snapshot summary, otherwise they are + # included (as this indicates that they are relevant to the + # application). + + all_of_them = [] # every single object + ref_counter = {} # how often it is referenced; (id(o), o) pairs + + def store_info(o): + all_of_them.append(o) + if id(o) in ref_counter: + ref_counter[id(o)] += 1 + else: + ref_counter[id(o)] = 1 + + # store infos on every single object related to the summaries + store_info(self.summaries) + for k, v in self.summaries.items(): + store_info(k) + summary._traverse(v, store_info) + + # do the summary + res = summary.summarize(muppy.get_objects()) + + # remove ids stored in the ref_counter + for _id in ref_counter: + # referenced in frame, ref_counter, ref_counter.keys() + if len(gc.get_referrers(_id)) == (3): + summary._subtract(res, _id) + for o in all_of_them: + # referenced in frame, summary, all_of_them + if len(gc.get_referrers(o)) == (ref_counter[id(o)] + 2): + summary._subtract(res, o) + + return res + + def diff(self, summary1=None, summary2=None): + """Compute diff between to summaries. + + If no summary is provided, the diff from the last to the current + summary is used. If summary1 is provided the diff from summary1 + to the current summary is used. If summary1 and summary2 are + provided, the diff between these two is used. + + """ + res = None + if summary2 is None: + self.s1 = self.create_summary() + if summary1 is None: + res = summary.get_diff(self.s0, self.s1) + else: + res = summary.get_diff(summary1, self.s1) + self.s0 = self.s1 + else: + if summary1 is not None: + res = summary.get_diff(summary1, summary2) + else: + raise ValueError( + "You cannot provide summary2 without summary1.") + return summary._sweep(res) + + def print_diff(self, summary1=None, summary2=None): + """Compute diff between to summaries and print it. + + If no summary is provided, the diff from the last to the current + summary is used. If summary1 is provided the diff from summary1 + to the current summary is used. If summary1 and summary2 are + provided, the diff between these two is used. + """ + summary.print_(self.diff(summary1=summary1, summary2=summary2)) + + def format_diff(self, summary1=None, summary2=None): + """Compute diff between to summaries and return a list of formatted + lines. + + If no summary is provided, the diff from the last to the current + summary is used. If summary1 is provided the diff from summary1 + to the current summary is used. If summary1 and summary2 are + provided, the diff between these two is used. + """ + return summary.format_(self.diff(summary1=summary1, summary2=summary2)) + + def store_summary(self, key): + """Store a current summary in self.summaries.""" + self.summaries[key] = self.create_summary() + + +class ObjectTracker(object): + """ + Helper class to track changes in the set of existing objects. + + Each time you invoke a diff with this tracker, the objects which existed + during the last invocation are compared with the objects which exist during + the current invocation. + + Please note that in order to do so, strong references to all objects will + be stored. This means that none of these objects can be garbage collected. + A use case for the ObjectTracker is the monitoring of a state which should + be stable, but you see new objects being created nevertheless. With the + ObjectTracker you can identify these new objects. + + """ + + # Some precaution needs to be taken when handling frame objects (see + # warning at http://docs.python.org/lib/inspect-stack.html). All ignore + # lists used need to be emptied so no frame objects remain referenced. + + def __init__(self): + """On initialisation, the current state of objects is stored. + + Note that all objects which exist at this point in time will not be + released until you destroy this ObjectTracker instance. + """ + self.o0 = self._get_objects(ignore=(inspect.currentframe(),)) + + def _get_objects(self, ignore=()): + """Get all currently existing objects. + + XXX - ToDo: This method is a copy&paste from muppy.get_objects, but + some modifications are applied. Specifically, it allows to ignore + objects (which includes the current frame). + + keyword arguments + ignore -- list of objects to ignore + """ + def remove_ignore(objects, ignore=()): + # remove all objects listed in the ignore list + res = [] + for o in objects: + if not compat.object_in_list(o, ignore): + res.append(o) + return res + + tmp = gc.get_objects() + ignore += (inspect.currentframe(), self, ignore, remove_ignore) + if hasattr(self, 'o0'): + ignore += (self.o0,) + if hasattr(self, 'o1'): + ignore += (self.o1,) + # this implies that referenced objects are also ignored + tmp = remove_ignore(tmp, ignore) + res = [] + for o in tmp: + # gc.get_objects returns only container objects, but we also want + # the objects referenced by them + refs = muppy.get_referents(o) + for ref in refs: + if not gc.is_tracked(ref): + # we already got the container objects, now we only add + # non-container objects + res.append(ref) + res.extend(tmp) + res = muppy._remove_duplicates(res) + if ignore is not None: + # repeat to filter out objects which may have been referenced + res = remove_ignore(res, ignore) + # manual cleanup, see comment above + del ignore + return res + + def get_diff(self, ignore=()): + """Get the diff to the last time the state of objects was measured. + + keyword arguments + ignore -- list of objects to ignore + """ + # ignore this and the caller frame + self.o1 = self._get_objects(ignore+(inspect.currentframe(),)) + diff = muppy.get_diff(self.o0, self.o1) + self.o0 = self.o1 + # manual cleanup, see comment above + return diff + + def print_diff(self, ignore=()): + """Print the diff to the last time the state of objects was measured. + + keyword arguments + ignore -- list of objects to ignore + """ + # ignore this and the caller frame + for line in self.format_diff(ignore+(inspect.currentframe(),)): + print(line) + + def format_diff(self, ignore=()): + """Format the diff to the last time the state of objects was measured. + + keyword arguments + ignore -- list of objects to ignore + """ + # ignore this and the caller frame + lines = [] + diff = self.get_diff(ignore+(inspect.currentframe(),)) + lines.append("Added objects:") + for line in summary.format_(summary.summarize(diff['+'])): + lines.append(line) + lines.append("Removed objects:") + for line in summary.format_(summary.summarize(diff['-'])): + lines.append(line) + return lines diff --git a/venv/lib/python3.9/site-packages/pympler/util/__init__.py b/venv/lib/python3.9/site-packages/pympler/util/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/util/__init__.py diff --git a/venv/lib/python3.9/site-packages/pympler/util/bottle.py b/venv/lib/python3.9/site-packages/pympler/util/bottle.py new file mode 100644 index 00000000..9806efd0 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/util/bottle.py @@ -0,0 +1,3771 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Bottle is a fast and simple micro-framework for small web applications. It +offers request dispatching (Routes) with url parameter support, templates, +a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and +template engines - all in a single file and with no dependencies other than the +Python Standard Library. + +Homepage and documentation: http://bottlepy.org/ + +Copyright (c) 2016, Marcel Hellkamp. +License: MIT (see LICENSE for details) +""" + +from __future__ import with_statement + +__author__ = 'Marcel Hellkamp' +__version__ = '0.12.19' +__license__ = 'MIT' + +# The gevent server adapter needs to patch some modules before they are imported +# This is why we parse the commandline parameters here but handle them later +if __name__ == '__main__': + from optparse import OptionParser + _cmd_parser = OptionParser(usage="usage: %prog [options] package.module:app") + _opt = _cmd_parser.add_option + _opt("--version", action="store_true", help="show version number.") + _opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") + _opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") + _opt("-p", "--plugin", action="append", help="install additional plugin/s.") + _opt("--debug", action="store_true", help="start server in debug mode.") + _opt("--reload", action="store_true", help="auto-reload on file changes.") + _cmd_options, _cmd_args = _cmd_parser.parse_args() + if _cmd_options.server and _cmd_options.server.startswith('gevent'): + import gevent.monkey; gevent.monkey.patch_all() + +import base64, cgi, email.utils, functools, hmac, itertools, mimetypes,\ + os, re, subprocess, sys, tempfile, threading, time, warnings, hashlib + +from datetime import date as datedate, datetime, timedelta +from tempfile import TemporaryFile +from traceback import format_exc, print_exc +from inspect import getargspec +from unicodedata import normalize + + +try: from simplejson import dumps as json_dumps, loads as json_lds +except ImportError: # pragma: no cover + try: from json import dumps as json_dumps, loads as json_lds + except ImportError: + try: from django.utils.simplejson import dumps as json_dumps, loads as json_lds + except ImportError: + def json_dumps(data): + raise ImportError("JSON support requires Python 2.6 or simplejson.") + json_lds = json_dumps + + + +# We now try to fix 2.5/2.6/3.1/3.2 incompatibilities. +# It ain't pretty but it works... Sorry for the mess. + +py = sys.version_info +py3k = py >= (3, 0, 0) +py25 = py < (2, 6, 0) +py31 = (3, 1, 0) <= py < (3, 2, 0) + +# Workaround for the missing "as" keyword in py3k. +def _e(): return sys.exc_info()[1] + +# Workaround for the "print is a keyword/function" Python 2/3 dilemma +# and a fallback for mod_wsgi (resticts stdout/err attribute access) +try: + _stdout, _stderr = sys.stdout.write, sys.stderr.write +except IOError: + _stdout = lambda x: sys.stdout.write(x) + _stderr = lambda x: sys.stderr.write(x) + +# Lots of stdlib and builtin differences. +if py3k: + import http.client as httplib + import _thread as thread + from urllib.parse import urljoin, SplitResult as UrlSplitResult + from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote + urlunquote = functools.partial(urlunquote, encoding='latin1') + from http.cookies import SimpleCookie + if py >= (3, 3, 0): + from collections.abc import MutableMapping as DictMixin + from types import ModuleType as new_module + else: + from collections import MutableMapping as DictMixin + from imp import new_module + import pickle + from io import BytesIO + from configparser import ConfigParser + basestring = str + unicode = str + json_loads = lambda s: json_lds(touni(s)) + callable = lambda x: hasattr(x, '__call__') + imap = map + def _raise(*a): raise a[0](a[1]).with_traceback(a[2]) +else: # 2.x + import httplib + import thread + from urlparse import urljoin, SplitResult as UrlSplitResult + from urllib import urlencode, quote as urlquote, unquote as urlunquote + from Cookie import SimpleCookie + from itertools import imap + import cPickle as pickle + from imp import new_module + from StringIO import StringIO as BytesIO + from ConfigParser import SafeConfigParser as ConfigParser + if py25: + msg = "Python 2.5 support may be dropped in future versions of Bottle." + warnings.warn(msg, DeprecationWarning) + from UserDict import DictMixin + def next(it): return it.next() + bytes = str + else: # 2.6, 2.7 + from collections import MutableMapping as DictMixin + unicode = unicode + json_loads = json_lds + eval(compile('def _raise(*a): raise a[0], a[1], a[2]', '<py3fix>', 'exec')) + +# Some helpers for string/byte handling +def tob(s, enc='utf8'): + return s.encode(enc) if isinstance(s, unicode) else bytes(s) +def touni(s, enc='utf8', err='strict'): + return s.decode(enc, err) if isinstance(s, bytes) else unicode(s) +tonat = touni if py3k else tob + +# 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense). +# 3.1 needs a workaround. +if py31: + from io import TextIOWrapper + class NCTextIOWrapper(TextIOWrapper): + def close(self): pass # Keep wrapped buffer open. + + +# A bug in functools causes it to break if the wrapper is an instance method +def update_wrapper(wrapper, wrapped, *a, **ka): + try: functools.update_wrapper(wrapper, wrapped, *a, **ka) + except AttributeError: pass + + + +# These helpers are used at module level and need to be defined first. +# And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. + +def depr(message, hard=False): + warnings.warn(message, DeprecationWarning, stacklevel=3) + +def makelist(data): # This is just to handy + if isinstance(data, (tuple, list, set, dict)): return list(data) + elif data: return [data] + else: return [] + + +class DictProperty(object): + ''' Property that maps to a key in a local dict-like attribute. ''' + def __init__(self, attr, key=None, read_only=False): + self.attr, self.key, self.read_only = attr, key, read_only + + def __call__(self, func): + functools.update_wrapper(self, func, updated=[]) + self.getter, self.key = func, self.key or func.__name__ + return self + + def __get__(self, obj, cls): + if obj is None: return self + key, storage = self.key, getattr(obj, self.attr) + if key not in storage: storage[key] = self.getter(obj) + return storage[key] + + def __set__(self, obj, value): + if self.read_only: raise AttributeError("Read-Only property.") + getattr(obj, self.attr)[self.key] = value + + def __delete__(self, obj): + if self.read_only: raise AttributeError("Read-Only property.") + del getattr(obj, self.attr)[self.key] + + +class cached_property(object): + ''' A property that is only computed once per instance and then replaces + itself with an ordinary attribute. Deleting the attribute resets the + property. ''' + + def __init__(self, func): + self.__doc__ = getattr(func, '__doc__') + self.func = func + + def __get__(self, obj, cls): + if obj is None: return self + value = obj.__dict__[self.func.__name__] = self.func(obj) + return value + + +class lazy_attribute(object): + ''' A property that caches itself to the class object. ''' + def __init__(self, func): + functools.update_wrapper(self, func, updated=[]) + self.getter = func + + def __get__(self, obj, cls): + value = self.getter(cls) + setattr(cls, self.__name__, value) + return value + + + + + + +############################################################################### +# Exceptions and Events ######################################################## +############################################################################### + + +class BottleException(Exception): + """ A base class for exceptions used by bottle. """ + pass + + + + + + +############################################################################### +# Routing ###################################################################### +############################################################################### + + +class RouteError(BottleException): + """ This is a base class for all routing related exceptions """ + + +class RouteReset(BottleException): + """ If raised by a plugin or request handler, the route is reset and all + plugins are re-applied. """ + +class RouterUnknownModeError(RouteError): pass + + +class RouteSyntaxError(RouteError): + """ The route parser found something not supported by this router. """ + + +class RouteBuildError(RouteError): + """ The route could not be built. """ + + +def _re_flatten(p): + ''' Turn all capturing groups in a regular expression pattern into + non-capturing groups. ''' + if '(' not in p: return p + return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', + lambda m: m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:', p) + + +class Router(object): + ''' A Router is an ordered collection of route->target pairs. It is used to + efficiently match WSGI requests against a number of routes and return + the first target that satisfies the request. The target may be anything, + usually a string, ID or callable object. A route consists of a path-rule + and a HTTP method. + + The path-rule is either a static path (e.g. `/contact`) or a dynamic + path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax + and details on the matching order are described in docs:`routing`. + ''' + + default_pattern = '[^/]+' + default_filter = 're' + + #: The current CPython regexp implementation does not allow more + #: than 99 matching groups per regular expression. + _MAX_GROUPS_PER_PATTERN = 99 + + def __init__(self, strict=False): + self.rules = [] # All rules in order + self._groups = {} # index of regexes to find them in dyna_routes + self.builder = {} # Data structure for the url builder + self.static = {} # Search structure for static routes + self.dyna_routes = {} + self.dyna_regexes = {} # Search structure for dynamic routes + #: If true, static routes are no longer checked first. + self.strict_order = strict + self.filters = { + 're': lambda conf: + (_re_flatten(conf or self.default_pattern), None, None), + 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))), + 'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))), + 'path': lambda conf: (r'.+?', None, None)} + + def add_filter(self, name, func): + ''' Add a filter. The provided function is called with the configuration + string as parameter and must return a (regexp, to_python, to_url) tuple. + The first element is a string, the last two are callables or None. ''' + self.filters[name] = func + + rule_syntax = re.compile('(\\\\*)'\ + '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'\ + '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'\ + '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') + + def _itertokens(self, rule): + offset, prefix = 0, '' + for match in self.rule_syntax.finditer(rule): + prefix += rule[offset:match.start()] + g = match.groups() + if len(g[0])%2: # Escaped wildcard + prefix += match.group(0)[len(g[0]):] + offset = match.end() + continue + if prefix: + yield prefix, None, None + name, filtr, conf = g[4:7] if g[2] is None else g[1:4] + yield name, filtr or 'default', conf or None + offset, prefix = match.end(), '' + if offset <= len(rule) or prefix: + yield prefix+rule[offset:], None, None + + def add(self, rule, method, target, name=None): + ''' Add a new rule or replace the target for an existing rule. ''' + anons = 0 # Number of anonymous wildcards found + keys = [] # Names of keys + pattern = '' # Regular expression pattern with named groups + filters = [] # Lists of wildcard input filters + builder = [] # Data structure for the URL builder + is_static = True + + for key, mode, conf in self._itertokens(rule): + if mode: + is_static = False + if mode == 'default': mode = self.default_filter + mask, in_filter, out_filter = self.filters[mode](conf) + if not key: + pattern += '(?:%s)' % mask + key = 'anon%d' % anons + anons += 1 + else: + pattern += '(?P<%s>%s)' % (key, mask) + keys.append(key) + if in_filter: filters.append((key, in_filter)) + builder.append((key, out_filter or str)) + elif key: + pattern += re.escape(key) + builder.append((None, key)) + + self.builder[rule] = builder + if name: self.builder[name] = builder + + if is_static and not self.strict_order: + self.static.setdefault(method, {}) + self.static[method][self.build(rule)] = (target, None) + return + + try: + re_pattern = re.compile('^(%s)$' % pattern) + re_match = re_pattern.match + except re.error: + raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, _e())) + + if filters: + def getargs(path): + url_args = re_match(path).groupdict() + for name, wildcard_filter in filters: + try: + url_args[name] = wildcard_filter(url_args[name]) + except ValueError: + raise HTTPError(400, 'Path has wrong format.') + return url_args + elif re_pattern.groupindex: + def getargs(path): + return re_match(path).groupdict() + else: + getargs = None + + flatpat = _re_flatten(pattern) + whole_rule = (rule, flatpat, target, getargs) + + if (flatpat, method) in self._groups: + if DEBUG: + msg = 'Route <%s %s> overwrites a previously defined route' + warnings.warn(msg % (method, rule), RuntimeWarning) + self.dyna_routes[method][self._groups[flatpat, method]] = whole_rule + else: + self.dyna_routes.setdefault(method, []).append(whole_rule) + self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1 + + self._compile(method) + + def _compile(self, method): + all_rules = self.dyna_routes[method] + comborules = self.dyna_regexes[method] = [] + maxgroups = self._MAX_GROUPS_PER_PATTERN + for x in range(0, len(all_rules), maxgroups): + some = all_rules[x:x+maxgroups] + combined = (flatpat for (_, flatpat, _, _) in some) + combined = '|'.join('(^%s$)' % flatpat for flatpat in combined) + combined = re.compile(combined).match + rules = [(target, getargs) for (_, _, target, getargs) in some] + comborules.append((combined, rules)) + + def build(self, _name, *anons, **query): + ''' Build an URL by filling the wildcards in a rule. ''' + builder = self.builder.get(_name) + if not builder: raise RouteBuildError("No route with that name.", _name) + try: + for i, value in enumerate(anons): query['anon%d'%i] = value + url = ''.join([f(query.pop(n)) if n else f for (n,f) in builder]) + return url if not query else url+'?'+urlencode(query) + except KeyError: + raise RouteBuildError('Missing URL argument: %r' % _e().args[0]) + + def match(self, environ): + ''' Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). ''' + verb = environ['REQUEST_METHOD'].upper() + path = environ['PATH_INFO'] or '/' + target = None + if verb == 'HEAD': + methods = ['PROXY', verb, 'GET', 'ANY'] + else: + methods = ['PROXY', verb, 'ANY'] + + for method in methods: + if method in self.static and path in self.static[method]: + target, getargs = self.static[method][path] + return target, getargs(path) if getargs else {} + elif method in self.dyna_regexes: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + target, getargs = rules[match.lastindex - 1] + return target, getargs(path) if getargs else {} + + # No matching route found. Collect alternative methods for 405 response + allowed = set([]) + nocheck = set(methods) + for method in set(self.static) - nocheck: + if path in self.static[method]: + allowed.add(method) + for method in set(self.dyna_regexes) - allowed - nocheck: + for combined, rules in self.dyna_regexes[method]: + match = combined(path) + if match: + allowed.add(method) + if allowed: + allow_header = ",".join(sorted(allowed)) + raise HTTPError(405, "Method not allowed.", Allow=allow_header) + + # No matching route and no alternative method found. We give up + raise HTTPError(404, "Not found: " + repr(path)) + + + + + + +class Route(object): + ''' This class wraps a route callback along with route specific metadata and + configuration and applies Plugins on demand. It is also responsible for + turing an URL path rule into a regular expression usable by the Router. + ''' + + def __init__(self, app, rule, method, callback, name=None, + plugins=None, skiplist=None, **config): + #: The application this route is installed to. + self.app = app + #: The path-rule string (e.g. ``/wiki/:page``). + self.rule = rule + #: The HTTP method as a string (e.g. ``GET``). + self.method = method + #: The original callback with no plugins applied. Useful for introspection. + self.callback = callback + #: The name of the route (if specified) or ``None``. + self.name = name or None + #: A list of route-specific plugins (see :meth:`Bottle.route`). + self.plugins = plugins or [] + #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). + self.skiplist = skiplist or [] + #: Additional keyword arguments passed to the :meth:`Bottle.route` + #: decorator are stored in this dictionary. Used for route-specific + #: plugin configuration and meta-data. + self.config = ConfigDict().load_dict(config, make_namespaces=True) + + def __call__(self, *a, **ka): + depr("Some APIs changed to return Route() instances instead of"\ + " callables. Make sure to use the Route.call method and not to"\ + " call Route instances directly.") #0.12 + return self.call(*a, **ka) + + @cached_property + def call(self): + ''' The route callback with all plugins applied. This property is + created on demand and then cached to speed up subsequent requests.''' + return self._make_callback() + + def reset(self): + ''' Forget any cached values. The next time :attr:`call` is accessed, + all plugins are re-applied. ''' + self.__dict__.pop('call', None) + + def prepare(self): + ''' Do all on-demand work immediately (useful for debugging).''' + self.call + + @property + def _context(self): + depr('Switch to Plugin API v2 and access the Route object directly.') #0.12 + return dict(rule=self.rule, method=self.method, callback=self.callback, + name=self.name, app=self.app, config=self.config, + apply=self.plugins, skip=self.skiplist) + + def all_plugins(self): + ''' Yield all Plugins affecting this route. ''' + unique = set() + for p in reversed(self.app.plugins + self.plugins): + if True in self.skiplist: break + name = getattr(p, 'name', False) + if name and (name in self.skiplist or name in unique): continue + if p in self.skiplist or type(p) in self.skiplist: continue + if name: unique.add(name) + yield p + + def _make_callback(self): + callback = self.callback + for plugin in self.all_plugins(): + try: + if hasattr(plugin, 'apply'): + api = getattr(plugin, 'api', 1) + context = self if api > 1 else self._context + callback = plugin.apply(callback, context) + else: + callback = plugin(callback) + except RouteReset: # Try again with changed configuration. + return self._make_callback() + if not callback is self.callback: + update_wrapper(callback, self.callback) + return callback + + def get_undecorated_callback(self): + ''' Return the callback. If the callback is a decorated function, try to + recover the original function. ''' + func = self.callback + func = getattr(func, '__func__' if py3k else 'im_func', func) + closure_attr = '__closure__' if py3k else 'func_closure' + while hasattr(func, closure_attr) and getattr(func, closure_attr): + func = getattr(func, closure_attr)[0].cell_contents + return func + + def get_callback_args(self): + ''' Return a list of argument names the callback (most likely) accepts + as keyword arguments. If the callback is a decorated function, try + to recover the original function before inspection. ''' + return getargspec(self.get_undecorated_callback())[0] + + def get_config(self, key, default=None): + ''' Lookup a config field and return its value, first checking the + route.config, then route.app.config.''' + for conf in (self.config, self.app.conifg): + if key in conf: return conf[key] + return default + + def __repr__(self): + cb = self.get_undecorated_callback() + return '<%s %r %r>' % (self.method, self.rule, cb) + + + + + + +############################################################################### +# Application Object ########################################################### +############################################################################### + + +class Bottle(object): + """ Each Bottle object represents a single, distinct web application and + consists of routes, callbacks, plugins, resources and configuration. + Instances are callable WSGI applications. + + :param catchall: If true (default), handle all exceptions. Turn off to + let debugging middleware handle exceptions. + """ + + def __init__(self, catchall=True, autojson=True): + + #: A :class:`ConfigDict` for app specific configuration. + self.config = ConfigDict() + self.config._on_change = functools.partial(self.trigger_hook, 'config') + self.config.meta_set('autojson', 'validate', bool) + self.config.meta_set('catchall', 'validate', bool) + self.config['catchall'] = catchall + self.config['autojson'] = autojson + + #: A :class:`ResourceManager` for application files + self.resources = ResourceManager() + + self.routes = [] # List of installed :class:`Route` instances. + self.router = Router() # Maps requests to :class:`Route` instances. + self.error_handler = {} + + # Core plugins + self.plugins = [] # List of installed plugins. + if self.config['autojson']: + self.install(JSONPlugin()) + self.install(TemplatePlugin()) + + #: If true, most exceptions are caught and returned as :exc:`HTTPError` + catchall = DictProperty('config', 'catchall') + + __hook_names = 'before_request', 'after_request', 'app_reset', 'config' + __hook_reversed = 'after_request' + + @cached_property + def _hooks(self): + return dict((name, []) for name in self.__hook_names) + + def add_hook(self, name, func): + ''' Attach a callback to a hook. Three hooks are currently implemented: + + before_request + Executed once before each request. The request context is + available, but no routing has happened yet. + after_request + Executed once after each request regardless of its outcome. + app_reset + Called whenever :meth:`Bottle.reset` is called. + ''' + if name in self.__hook_reversed: + self._hooks[name].insert(0, func) + else: + self._hooks[name].append(func) + + def remove_hook(self, name, func): + ''' Remove a callback from a hook. ''' + if name in self._hooks and func in self._hooks[name]: + self._hooks[name].remove(func) + return True + + def trigger_hook(self, __name, *args, **kwargs): + ''' Trigger a hook and return a list of results. ''' + return [hook(*args, **kwargs) for hook in self._hooks[__name][:]] + + def hook(self, name): + """ Return a decorator that attaches a callback to a hook. See + :meth:`add_hook` for details.""" + def decorator(func): + self.add_hook(name, func) + return func + return decorator + + def mount(self, prefix, app, **options): + ''' Mount an application (:class:`Bottle` or plain WSGI) to a specific + URL prefix. Example:: + + root_app.mount('/admin/', admin_app) + + :param prefix: path prefix or `mount-point`. If it ends in a slash, + that slash is mandatory. + :param app: an instance of :class:`Bottle` or a WSGI application. + + All other parameters are passed to the underlying :meth:`route` call. + ''' + if isinstance(app, basestring): + depr('Parameter order of Bottle.mount() changed.', True) # 0.10 + + segments = [p for p in prefix.split('/') if p] + if not segments: raise ValueError('Empty path prefix.') + path_depth = len(segments) + + def mountpoint_wrapper(): + try: + request.path_shift(path_depth) + rs = HTTPResponse([]) + def start_response(status, headerlist, exc_info=None): + if exc_info: + try: + _raise(*exc_info) + finally: + exc_info = None + rs.status = status + for name, value in headerlist: rs.add_header(name, value) + return rs.body.append + body = app(request.environ, start_response) + if body and rs.body: body = itertools.chain(rs.body, body) + rs.body = body or rs.body + return rs + finally: + request.path_shift(-path_depth) + + options.setdefault('skip', True) + options.setdefault('method', 'PROXY') + options.setdefault('mountpoint', {'prefix': prefix, 'target': app}) + options['callback'] = mountpoint_wrapper + + self.route('/%s/<:re:.*>' % '/'.join(segments), **options) + if not prefix.endswith('/'): + self.route('/' + '/'.join(segments), **options) + + def merge(self, routes): + ''' Merge the routes of another :class:`Bottle` application or a list of + :class:`Route` objects into this application. The routes keep their + 'owner', meaning that the :data:`Route.app` attribute is not + changed. ''' + if isinstance(routes, Bottle): + routes = routes.routes + for route in routes: + self.add_route(route) + + def install(self, plugin): + ''' Add a plugin to the list of plugins and prepare it for being + applied to all routes of this application. A plugin may be a simple + decorator or an object that implements the :class:`Plugin` API. + ''' + if hasattr(plugin, 'setup'): plugin.setup(self) + if not callable(plugin) and not hasattr(plugin, 'apply'): + raise TypeError("Plugins must be callable or implement .apply()") + self.plugins.append(plugin) + self.reset() + return plugin + + def uninstall(self, plugin): + ''' Uninstall plugins. Pass an instance to remove a specific plugin, a type + object to remove all plugins that match that type, a string to remove + all plugins with a matching ``name`` attribute or ``True`` to remove all + plugins. Return the list of removed plugins. ''' + removed, remove = [], plugin + for i, plugin in list(enumerate(self.plugins))[::-1]: + if remove is True or remove is plugin or remove is type(plugin) \ + or getattr(plugin, 'name', True) == remove: + removed.append(plugin) + del self.plugins[i] + if hasattr(plugin, 'close'): plugin.close() + if removed: self.reset() + return removed + + def reset(self, route=None): + ''' Reset all routes (force plugins to be re-applied) and clear all + caches. If an ID or route object is given, only that specific route + is affected. ''' + if route is None: routes = self.routes + elif isinstance(route, Route): routes = [route] + else: routes = [self.routes[route]] + for route in routes: route.reset() + if DEBUG: + for route in routes: route.prepare() + self.trigger_hook('app_reset') + + def close(self): + ''' Close the application and all installed plugins. ''' + for plugin in self.plugins: + if hasattr(plugin, 'close'): plugin.close() + self.stopped = True + + def run(self, **kwargs): + ''' Calls :func:`run` with the same parameters. ''' + run(self, **kwargs) + + def match(self, environ): + """ Search for a matching route and return a (:class:`Route` , urlargs) + tuple. The second value is a dictionary with parameters extracted + from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" + return self.router.match(environ) + + def get_url(self, routename, **kargs): + """ Return a string that matches a named route """ + scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/' + location = self.router.build(routename, **kargs).lstrip('/') + return urljoin(urljoin('/', scriptname), location) + + def add_route(self, route): + ''' Add a route object, but do not change the :data:`Route.app` + attribute.''' + self.routes.append(route) + self.router.add(route.rule, route.method, route, name=route.name) + if DEBUG: route.prepare() + + def route(self, path=None, method='GET', callback=None, name=None, + apply=None, skip=None, **config): + """ A decorator to bind a function to a request URL. Example:: + + @app.route('/hello/:name') + def hello(name): + return 'Hello %s' % name + + The ``:name`` part is a wildcard. See :class:`Router` for syntax + details. + + :param path: Request path or a list of paths to listen to. If no + path is specified, it is automatically generated from the + signature of the function. + :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of + methods to listen to. (default: `GET`) + :param callback: An optional shortcut to avoid the decorator + syntax. ``route(..., callback=func)`` equals ``route(...)(func)`` + :param name: The name for this route. (default: None) + :param apply: A decorator or plugin or a list of plugins. These are + applied to the route callback in addition to installed plugins. + :param skip: A list of plugins, plugin classes or names. Matching + plugins are not installed to this route. ``True`` skips all. + + Any additional keyword arguments are stored as route-specific + configuration and passed to plugins (see :meth:`Plugin.apply`). + """ + if callable(path): path, callback = None, path + plugins = makelist(apply) + skiplist = makelist(skip) + def decorator(callback): + # TODO: Documentation and tests + if isinstance(callback, basestring): callback = load(callback) + for rule in makelist(path) or yieldroutes(callback): + for verb in makelist(method): + verb = verb.upper() + route = Route(self, rule, verb, callback, name=name, + plugins=plugins, skiplist=skiplist, **config) + self.add_route(route) + return callback + return decorator(callback) if callback else decorator + + def get(self, path=None, method='GET', **options): + """ Equals :meth:`route`. """ + return self.route(path, method, **options) + + def post(self, path=None, method='POST', **options): + """ Equals :meth:`route` with a ``POST`` method parameter. """ + return self.route(path, method, **options) + + def put(self, path=None, method='PUT', **options): + """ Equals :meth:`route` with a ``PUT`` method parameter. """ + return self.route(path, method, **options) + + def delete(self, path=None, method='DELETE', **options): + """ Equals :meth:`route` with a ``DELETE`` method parameter. """ + return self.route(path, method, **options) + + def error(self, code=500): + """ Decorator: Register an output handler for a HTTP error code""" + def wrapper(handler): + self.error_handler[int(code)] = handler + return handler + return wrapper + + def default_error_handler(self, res): + return tob(template(ERROR_PAGE_TEMPLATE, e=res)) + + def _handle(self, environ): + path = environ['bottle.raw_path'] = environ['PATH_INFO'] + if py3k: + try: + environ['PATH_INFO'] = path.encode('latin1').decode('utf8') + except UnicodeError: + return HTTPError(400, 'Invalid path string. Expected UTF-8') + + try: + environ['bottle.app'] = self + request.bind(environ) + response.bind() + try: + self.trigger_hook('before_request') + route, args = self.router.match(environ) + environ['route.handle'] = route + environ['bottle.route'] = route + environ['route.url_args'] = args + return route.call(**args) + finally: + self.trigger_hook('after_request') + + except HTTPResponse: + return _e() + except RouteReset: + route.reset() + return self._handle(environ) + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception: + if not self.catchall: raise + stacktrace = format_exc() + environ['wsgi.errors'].write(stacktrace) + return HTTPError(500, "Internal Server Error", _e(), stacktrace) + + def _cast(self, out, peek=None): + """ Try to convert the parameter into something WSGI compatible and set + correct HTTP headers when possible. + Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like, + iterable of strings and iterable of unicodes + """ + + # Empty output is done here + if not out: + if 'Content-Length' not in response: + response['Content-Length'] = 0 + return [] + # Join lists of byte or unicode strings. Mixed lists are NOT supported + if isinstance(out, (tuple, list))\ + and isinstance(out[0], (bytes, unicode)): + out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' + # Encode unicode strings + if isinstance(out, unicode): + out = out.encode(response.charset) + # Byte Strings are just returned + if isinstance(out, bytes): + if 'Content-Length' not in response: + response['Content-Length'] = len(out) + return [out] + # HTTPError or HTTPException (recursive, because they may wrap anything) + # TODO: Handle these explicitly in handle() or make them iterable. + if isinstance(out, HTTPError): + out.apply(response) + out = self.error_handler.get(out.status_code, self.default_error_handler)(out) + return self._cast(out) + if isinstance(out, HTTPResponse): + out.apply(response) + return self._cast(out.body) + + # File-like objects. + if hasattr(out, 'read'): + if 'wsgi.file_wrapper' in request.environ: + return request.environ['wsgi.file_wrapper'](out) + elif hasattr(out, 'close') or not hasattr(out, '__iter__'): + return WSGIFileWrapper(out) + + # Handle Iterables. We peek into them to detect their inner type. + try: + iout = iter(out) + first = next(iout) + while not first: + first = next(iout) + except StopIteration: + return self._cast('') + except HTTPResponse: + first = _e() + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception: + if not self.catchall: raise + first = HTTPError(500, 'Unhandled exception', _e(), format_exc()) + + # These are the inner types allowed in iterator or generator objects. + if isinstance(first, HTTPResponse): + return self._cast(first) + elif isinstance(first, bytes): + new_iter = itertools.chain([first], iout) + elif isinstance(first, unicode): + encoder = lambda x: x.encode(response.charset) + new_iter = imap(encoder, itertools.chain([first], iout)) + else: + msg = 'Unsupported response type: %s' % type(first) + return self._cast(HTTPError(500, msg)) + if hasattr(out, 'close'): + new_iter = _closeiter(new_iter, out.close) + return new_iter + + def wsgi(self, environ, start_response): + """ The bottle WSGI-interface. """ + try: + out = self._cast(self._handle(environ)) + # rfc2616 section 4.3 + if response._status_code in (100, 101, 204, 304)\ + or environ['REQUEST_METHOD'] == 'HEAD': + if hasattr(out, 'close'): out.close() + out = [] + start_response(response._status_line, response.headerlist) + return out + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception: + if not self.catchall: raise + err = '<h1>Critical error while processing request: %s</h1>' \ + % html_escape(environ.get('PATH_INFO', '/')) + if DEBUG: + err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \ + '<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \ + % (html_escape(repr(_e())), html_escape(format_exc())) + environ['wsgi.errors'].write(err) + headers = [('Content-Type', 'text/html; charset=UTF-8')] + start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info()) + return [tob(err)] + + def __call__(self, environ, start_response): + ''' Each instance of :class:'Bottle' is a WSGI application. ''' + return self.wsgi(environ, start_response) + + + + + + +############################################################################### +# HTTP and WSGI Tools ########################################################## +############################################################################### + +class BaseRequest(object): + """ A wrapper for WSGI environment dictionaries that adds a lot of + convenient access methods and properties. Most of them are read-only. + + Adding new attributes to a request actually adds them to the environ + dictionary (as 'bottle.request.ext.<name>'). This is the recommended + way to store and access request-specific data. + """ + + __slots__ = ('environ') + + #: Maximum size of memory buffer for :attr:`body` in bytes. + MEMFILE_MAX = 102400 + + def __init__(self, environ=None): + """ Wrap a WSGI environ dictionary. """ + #: The wrapped WSGI environ dictionary. This is the only real attribute. + #: All other attributes actually are read-only properties. + self.environ = {} if environ is None else environ + self.environ['bottle.request'] = self + + @DictProperty('environ', 'bottle.app', read_only=True) + def app(self): + ''' Bottle application handling this request. ''' + raise RuntimeError('This request is not connected to an application.') + + @DictProperty('environ', 'bottle.route', read_only=True) + def route(self): + """ The bottle :class:`Route` object that matches this request. """ + raise RuntimeError('This request is not connected to a route.') + + @DictProperty('environ', 'route.url_args', read_only=True) + def url_args(self): + """ The arguments extracted from the URL. """ + raise RuntimeError('This request is not connected to a route.') + + @property + def path(self): + ''' The value of ``PATH_INFO`` with exactly one prefixed slash (to fix + broken clients and avoid the "empty path" edge case). ''' + return '/' + self.environ.get('PATH_INFO','').lstrip('/') + + @property + def method(self): + ''' The ``REQUEST_METHOD`` value as an uppercase string. ''' + return self.environ.get('REQUEST_METHOD', 'GET').upper() + + @DictProperty('environ', 'bottle.request.headers', read_only=True) + def headers(self): + ''' A :class:`WSGIHeaderDict` that provides case-insensitive access to + HTTP request headers. ''' + return WSGIHeaderDict(self.environ) + + def get_header(self, name, default=None): + ''' Return the value of a request header, or a given default value. ''' + return self.headers.get(name, default) + + @DictProperty('environ', 'bottle.request.cookies', read_only=True) + def cookies(self): + """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT + decoded. Use :meth:`get_cookie` if you expect signed cookies. """ + cookies = SimpleCookie(self.environ.get('HTTP_COOKIE','')).values() + return FormsDict((c.key, c.value) for c in cookies) + + def get_cookie(self, key, default=None, secret=None): + """ Return the content of a cookie. To read a `Signed Cookie`, the + `secret` must match the one used to create the cookie (see + :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing + cookie or wrong signature), return a default value. """ + value = self.cookies.get(key) + if secret and value: + dec = cookie_decode(value, secret) # (key, value) tuple or None + return dec[1] if dec and dec[0] == key else default + return value or default + + @DictProperty('environ', 'bottle.request.query', read_only=True) + def query(self): + ''' The :attr:`query_string` parsed into a :class:`FormsDict`. These + values are sometimes called "URL arguments" or "GET parameters", but + not to be confused with "URL wildcards" as they are provided by the + :class:`Router`. ''' + get = self.environ['bottle.get'] = FormsDict() + pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) + for key, value in pairs: + get[key] = value + return get + + @DictProperty('environ', 'bottle.request.forms', read_only=True) + def forms(self): + """ Form values parsed from an `url-encoded` or `multipart/form-data` + encoded POST or PUT request body. The result is returned as a + :class:`FormsDict`. All keys and values are strings. File uploads + are stored separately in :attr:`files`. """ + forms = FormsDict() + for name, item in self.POST.allitems(): + if not isinstance(item, FileUpload): + forms[name] = item + return forms + + @DictProperty('environ', 'bottle.request.params', read_only=True) + def params(self): + """ A :class:`FormsDict` with the combined values of :attr:`query` and + :attr:`forms`. File uploads are stored in :attr:`files`. """ + params = FormsDict() + for key, value in self.query.allitems(): + params[key] = value + for key, value in self.forms.allitems(): + params[key] = value + return params + + @DictProperty('environ', 'bottle.request.files', read_only=True) + def files(self): + """ File uploads parsed from `multipart/form-data` encoded POST or PUT + request body. The values are instances of :class:`FileUpload`. + + """ + files = FormsDict() + for name, item in self.POST.allitems(): + if isinstance(item, FileUpload): + files[name] = item + return files + + @DictProperty('environ', 'bottle.request.json', read_only=True) + def json(self): + ''' If the ``Content-Type`` header is ``application/json``, this + property holds the parsed content of the request body. Only requests + smaller than :attr:`MEMFILE_MAX` are processed to avoid memory + exhaustion. ''' + ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0] + if ctype == 'application/json': + b = self._get_body_string() + if not b: + return None + return json_loads(b) + return None + + def _iter_body(self, read, bufsize): + maxread = max(0, self.content_length) + while maxread: + part = read(min(maxread, bufsize)) + if not part: break + yield part + maxread -= len(part) + + def _iter_chunked(self, read, bufsize): + err = HTTPError(400, 'Error while parsing chunked transfer body.') + rn, sem, bs = tob('\r\n'), tob(';'), tob('') + while True: + header = read(1) + while header[-2:] != rn: + c = read(1) + header += c + if not c: raise err + if len(header) > bufsize: raise err + size, _, _ = header.partition(sem) + try: + maxread = int(tonat(size.strip()), 16) + except ValueError: + raise err + if maxread == 0: break + buff = bs + while maxread > 0: + if not buff: + buff = read(min(maxread, bufsize)) + part, buff = buff[:maxread], buff[maxread:] + if not part: raise err + yield part + maxread -= len(part) + if read(2) != rn: + raise err + + @DictProperty('environ', 'bottle.request.body', read_only=True) + def _body(self): + body_iter = self._iter_chunked if self.chunked else self._iter_body + read_func = self.environ['wsgi.input'].read + body, body_size, is_temp_file = BytesIO(), 0, False + for part in body_iter(read_func, self.MEMFILE_MAX): + body.write(part) + body_size += len(part) + if not is_temp_file and body_size > self.MEMFILE_MAX: + body, tmp = TemporaryFile(mode='w+b'), body + body.write(tmp.getvalue()) + del tmp + is_temp_file = True + self.environ['wsgi.input'] = body + body.seek(0) + return body + + def _get_body_string(self): + ''' read body until content-length or MEMFILE_MAX into a string. Raise + HTTPError(413) on requests that are to large. ''' + clen = self.content_length + if clen > self.MEMFILE_MAX: + raise HTTPError(413, 'Request to large') + if clen < 0: clen = self.MEMFILE_MAX + 1 + data = self.body.read(clen) + if len(data) > self.MEMFILE_MAX: # Fail fast + raise HTTPError(413, 'Request to large') + return data + + @property + def body(self): + """ The HTTP request body as a seek-able file-like object. Depending on + :attr:`MEMFILE_MAX`, this is either a temporary file or a + :class:`io.BytesIO` instance. Accessing this property for the first + time reads and replaces the ``wsgi.input`` environ variable. + Subsequent accesses just do a `seek(0)` on the file object. """ + self._body.seek(0) + return self._body + + @property + def chunked(self): + ''' True if Chunked transfer encoding was. ''' + return 'chunked' in self.environ.get('HTTP_TRANSFER_ENCODING', '').lower() + + #: An alias for :attr:`query`. + GET = query + + @DictProperty('environ', 'bottle.request.post', read_only=True) + def POST(self): + """ The values of :attr:`forms` and :attr:`files` combined into a single + :class:`FormsDict`. Values are either strings (form values) or + instances of :class:`cgi.FieldStorage` (file uploads). + """ + post = FormsDict() + # We default to application/x-www-form-urlencoded for everything that + # is not multipart and take the fast path (also: 3.1 workaround) + if not self.content_type.startswith('multipart/'): + pairs = _parse_qsl(tonat(self._get_body_string(), 'latin1')) + for key, value in pairs: + post[key] = value + return post + + safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi + for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): + if key in self.environ: safe_env[key] = self.environ[key] + args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) + if py31: + args['fp'] = NCTextIOWrapper(args['fp'], encoding='utf8', + newline='\n') + elif py3k: + args['encoding'] = 'utf8' + data = cgi.FieldStorage(**args) + self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394#msg207958 + data = data.list or [] + for item in data: + if item.filename: + post[item.name] = FileUpload(item.file, item.name, + item.filename, item.headers) + else: + post[item.name] = item.value + return post + + @property + def url(self): + """ The full request URI including hostname and scheme. If your app + lives behind a reverse proxy or load balancer and you get confusing + results, make sure that the ``X-Forwarded-Host`` header is set + correctly. """ + return self.urlparts.geturl() + + @DictProperty('environ', 'bottle.request.urlparts', read_only=True) + def urlparts(self): + ''' The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. + The tuple contains (scheme, host, path, query_string and fragment), + but the fragment is always empty because it is not visible to the + server. ''' + env = self.environ + http = env.get('HTTP_X_FORWARDED_PROTO') or env.get('wsgi.url_scheme', 'http') + host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') + if not host: + # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. + host = env.get('SERVER_NAME', '127.0.0.1') + port = env.get('SERVER_PORT') + if port and port != ('80' if http == 'http' else '443'): + host += ':' + port + path = urlquote(self.fullpath) + return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') + + @property + def fullpath(self): + """ Request path including :attr:`script_name` (if present). """ + return urljoin(self.script_name, self.path.lstrip('/')) + + @property + def query_string(self): + """ The raw :attr:`query` part of the URL (everything in between ``?`` + and ``#``) as a string. """ + return self.environ.get('QUERY_STRING', '') + + @property + def script_name(self): + ''' The initial portion of the URL's `path` that was removed by a higher + level (server or routing middleware) before the application was + called. This script path is returned with leading and tailing + slashes. ''' + script_name = self.environ.get('SCRIPT_NAME', '').strip('/') + return '/' + script_name + '/' if script_name else '/' + + def path_shift(self, shift=1): + ''' Shift path segments from :attr:`path` to :attr:`script_name` and + vice versa. + + :param shift: The number of path segments to shift. May be negative + to change the shift direction. (default: 1) + ''' + script = self.environ.get('SCRIPT_NAME','/') + self['SCRIPT_NAME'], self['PATH_INFO'] = path_shift(script, self.path, shift) + + @property + def content_length(self): + ''' The request body length as an integer. The client is responsible to + set this header. Otherwise, the real length of the body is unknown + and -1 is returned. In this case, :attr:`body` will be empty. ''' + return int(self.environ.get('CONTENT_LENGTH') or -1) + + @property + def content_type(self): + ''' The Content-Type header as a lowercase-string (default: empty). ''' + return self.environ.get('CONTENT_TYPE', '').lower() + + @property + def is_xhr(self): + ''' True if the request was triggered by a XMLHttpRequest. This only + works with JavaScript libraries that support the `X-Requested-With` + header (most of the popular libraries do). ''' + requested_with = self.environ.get('HTTP_X_REQUESTED_WITH','') + return requested_with.lower() == 'xmlhttprequest' + + @property + def is_ajax(self): + ''' Alias for :attr:`is_xhr`. "Ajax" is not the right term. ''' + return self.is_xhr + + @property + def auth(self): + """ HTTP authentication data as a (user, password) tuple. This + implementation currently supports basic (not digest) authentication + only. If the authentication happened at a higher level (e.g. in the + front web-server or a middleware), the password field is None, but + the user field is looked up from the ``REMOTE_USER`` environ + variable. On any errors, None is returned. """ + basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION','')) + if basic: return basic + ruser = self.environ.get('REMOTE_USER') + if ruser: return (ruser, None) + return None + + @property + def remote_route(self): + """ A list of all IPs that were involved in this request, starting with + the client IP and followed by zero or more proxies. This does only + work if all proxies support the ```X-Forwarded-For`` header. Note + that this information can be forged by malicious clients. """ + proxy = self.environ.get('HTTP_X_FORWARDED_FOR') + if proxy: return [ip.strip() for ip in proxy.split(',')] + remote = self.environ.get('REMOTE_ADDR') + return [remote] if remote else [] + + @property + def remote_addr(self): + """ The client IP as a string. Note that this information can be forged + by malicious clients. """ + route = self.remote_route + return route[0] if route else None + + def copy(self): + """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """ + return Request(self.environ.copy()) + + def get(self, value, default=None): return self.environ.get(value, default) + def __getitem__(self, key): return self.environ[key] + def __delitem__(self, key): self[key] = ""; del(self.environ[key]) + def __iter__(self): return iter(self.environ) + def __len__(self): return len(self.environ) + def keys(self): return self.environ.keys() + def __setitem__(self, key, value): + """ Change an environ value and clear all caches that depend on it. """ + + if self.environ.get('bottle.request.readonly'): + raise KeyError('The environ dictionary is read-only.') + + self.environ[key] = value + todelete = () + + if key == 'wsgi.input': + todelete = ('body', 'forms', 'files', 'params', 'post', 'json') + elif key == 'QUERY_STRING': + todelete = ('query', 'params') + elif key.startswith('HTTP_'): + todelete = ('headers', 'cookies') + + for key in todelete: + self.environ.pop('bottle.request.'+key, None) + + def __repr__(self): + return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url) + + def __getattr__(self, name): + ''' Search in self.environ for additional user defined attributes. ''' + try: + var = self.environ['bottle.request.ext.%s'%name] + return var.__get__(self) if hasattr(var, '__get__') else var + except KeyError: + raise AttributeError('Attribute %r not defined.' % name) + + def __setattr__(self, name, value): + if name == 'environ': return object.__setattr__(self, name, value) + self.environ['bottle.request.ext.%s'%name] = value + + +def _hkey(key): + if '\n' in key or '\r' in key or '\0' in key: + raise ValueError("Header names must not contain control characters: %r" % key) + return key.title().replace('_', '-') + + +def _hval(value): + value = tonat(value) + if '\n' in value or '\r' in value or '\0' in value: + raise ValueError("Header value must not contain control characters: %r" % value) + return value + + + +class HeaderProperty(object): + def __init__(self, name, reader=None, writer=None, default=''): + self.name, self.default = name, default + self.reader, self.writer = reader, writer + self.__doc__ = 'Current value of the %r header.' % name.title() + + def __get__(self, obj, cls): + if obj is None: return self + value = obj.get_header(self.name, self.default) + return self.reader(value) if self.reader else value + + def __set__(self, obj, value): + obj[self.name] = self.writer(value) if self.writer else value + + def __delete__(self, obj): + del obj[self.name] + + +class BaseResponse(object): + """ Storage class for a response body as well as headers and cookies. + + This class does support dict-like case-insensitive item-access to + headers, but is NOT a dict. Most notably, iterating over a response + yields parts of the body and not the headers. + + :param body: The response body as one of the supported types. + :param status: Either an HTTP status code (e.g. 200) or a status line + including the reason phrase (e.g. '200 OK'). + :param headers: A dictionary or a list of name-value pairs. + + Additional keyword arguments are added to the list of headers. + Underscores in the header name are replaced with dashes. + """ + + default_status = 200 + default_content_type = 'text/html; charset=UTF-8' + + # Header blacklist for specific response codes + # (rfc2616 section 10.2.3 and 10.3.5) + bad_headers = { + 204: set(('Content-Type',)), + 304: set(('Allow', 'Content-Encoding', 'Content-Language', + 'Content-Length', 'Content-Range', 'Content-Type', + 'Content-Md5', 'Last-Modified'))} + + def __init__(self, body='', status=None, headers=None, **more_headers): + self._cookies = None + self._headers = {} + self.body = body + self.status = status or self.default_status + if headers: + if isinstance(headers, dict): + headers = headers.items() + for name, value in headers: + self.add_header(name, value) + if more_headers: + for name, value in more_headers.items(): + self.add_header(name, value) + + def copy(self, cls=None): + ''' Returns a copy of self. ''' + cls = cls or BaseResponse + assert issubclass(cls, BaseResponse) + copy = cls() + copy.status = self.status + copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) + if self._cookies: + copy._cookies = SimpleCookie() + copy._cookies.load(self._cookies.output(header='')) + return copy + + def __iter__(self): + return iter(self.body) + + def close(self): + if hasattr(self.body, 'close'): + self.body.close() + + @property + def status_line(self): + ''' The HTTP status line as a string (e.g. ``404 Not Found``).''' + return self._status_line + + @property + def status_code(self): + ''' The HTTP status code as an integer (e.g. 404).''' + return self._status_code + + def _set_status(self, status): + if isinstance(status, int): + code, status = status, _HTTP_STATUS_LINES.get(status) + elif ' ' in status: + status = status.strip() + code = int(status.split()[0]) + else: + raise ValueError('String status line without a reason phrase.') + if not 100 <= code <= 999: raise ValueError('Status code out of range.') + self._status_code = code + self._status_line = str(status or ('%d Unknown' % code)) + + def _get_status(self): + return self._status_line + + status = property(_get_status, _set_status, None, + ''' A writeable property to change the HTTP response status. It accepts + either a numeric code (100-999) or a string with a custom reason + phrase (e.g. "404 Brain not found"). Both :data:`status_line` and + :data:`status_code` are updated accordingly. The return value is + always a status string. ''') + del _get_status, _set_status + + @property + def headers(self): + ''' An instance of :class:`HeaderDict`, a case-insensitive dict-like + view on the response headers. ''' + hdict = HeaderDict() + hdict.dict = self._headers + return hdict + + def __contains__(self, name): return _hkey(name) in self._headers + def __delitem__(self, name): del self._headers[_hkey(name)] + def __getitem__(self, name): return self._headers[_hkey(name)][-1] + def __setitem__(self, name, value): self._headers[_hkey(name)] = [_hval(value)] + + def get_header(self, name, default=None): + ''' Return the value of a previously defined header. If there is no + header with that name, return a default value. ''' + return self._headers.get(_hkey(name), [default])[-1] + + def set_header(self, name, value): + ''' Create a new response header, replacing any previously defined + headers with the same name. ''' + self._headers[_hkey(name)] = [_hval(value)] + + def add_header(self, name, value): + ''' Add an additional response header, not removing duplicates. ''' + self._headers.setdefault(_hkey(name), []).append(_hval(value)) + + def iter_headers(self): + ''' Yield (header, value) tuples, skipping headers that are not + allowed with the current response status code. ''' + return self.headerlist + + @property + def headerlist(self): + """ WSGI conform list of (header, value) tuples. """ + out = [] + headers = list(self._headers.items()) + if 'Content-Type' not in self._headers: + headers.append(('Content-Type', [self.default_content_type])) + if self._status_code in self.bad_headers: + bad_headers = self.bad_headers[self._status_code] + headers = [h for h in headers if h[0] not in bad_headers] + out += [(name, val) for (name, vals) in headers for val in vals] + if self._cookies: + for c in self._cookies.values(): + out.append(('Set-Cookie', _hval(c.OutputString()))) + if py3k: + out = [(k, v.encode('utf8').decode('latin1')) for (k, v) in out] + return out + + content_type = HeaderProperty('Content-Type') + content_length = HeaderProperty('Content-Length', reader=int) + expires = HeaderProperty('Expires', + reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), + writer=lambda x: http_date(x)) + + @property + def charset(self, default='UTF-8'): + """ Return the charset specified in the content-type header (default: utf8). """ + if 'charset=' in self.content_type: + return self.content_type.split('charset=')[-1].split(';')[0].strip() + return default + + def set_cookie(self, name, value, secret=None, **options): + ''' Create a new cookie or replace an old one. If the `secret` parameter is + set, create a `Signed Cookie` (described below). + + :param name: the name of the cookie. + :param value: the value of the cookie. + :param secret: a signature key required for signed cookies. + + Additionally, this method accepts all RFC 2109 attributes that are + supported by :class:`cookie.Morsel`, including: + + :param max_age: maximum age in seconds. (default: None) + :param expires: a datetime object or UNIX timestamp. (default: None) + :param domain: the domain that is allowed to read the cookie. + (default: current domain) + :param path: limits the cookie to a given path (default: current path) + :param secure: limit the cookie to HTTPS connections (default: off). + :param httponly: prevents client-side javascript to read this cookie + (default: off, requires Python 2.6 or newer). + + If neither `expires` nor `max_age` is set (default), the cookie will + expire at the end of the browser session (as soon as the browser + window is closed). + + Signed cookies may store any pickle-able object and are + cryptographically signed to prevent manipulation. Keep in mind that + cookies are limited to 4kb in most browsers. + + Warning: Signed cookies are not encrypted (the client can still see + the content) and not copy-protected (the client can restore an old + cookie). The main intention is to make pickling and unpickling + save, not to store secret information at client side. + ''' + if not self._cookies: + self._cookies = SimpleCookie() + + if secret: + value = touni(cookie_encode((name, value), secret)) + elif not isinstance(value, basestring): + raise TypeError('Secret key missing for non-string Cookie.') + + if len(value) > 4096: raise ValueError('Cookie value to long.') + self._cookies[name] = value + + for key, value in options.items(): + if key == 'max_age': + if isinstance(value, timedelta): + value = value.seconds + value.days * 24 * 3600 + if key == 'expires': + if isinstance(value, (datedate, datetime)): + value = value.timetuple() + elif isinstance(value, (int, float)): + value = time.gmtime(value) + value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) + self._cookies[name][key.replace('_', '-')] = value + + def delete_cookie(self, key, **kwargs): + ''' Delete a cookie. Be sure to use the same `domain` and `path` + settings as used to create the cookie. ''' + kwargs['max_age'] = -1 + kwargs['expires'] = 0 + self.set_cookie(key, '', **kwargs) + + def __repr__(self): + out = '' + for name, value in self.headerlist: + out += '%s: %s\n' % (name.title(), value.strip()) + return out + + +def local_property(name=None): + if name: depr('local_property() is deprecated and will be removed.') #0.12 + ls = threading.local() + def fget(self): + try: return ls.var + except AttributeError: + raise RuntimeError("Request context not initialized.") + def fset(self, value): ls.var = value + def fdel(self): del ls.var + return property(fget, fset, fdel, 'Thread-local property') + + +class LocalRequest(BaseRequest): + ''' A thread-local subclass of :class:`BaseRequest` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`request`). If accessed during a + request/response cycle, this instance always refers to the *current* + request (even on a multithreaded server). ''' + bind = BaseRequest.__init__ + environ = local_property() + + +class LocalResponse(BaseResponse): + ''' A thread-local subclass of :class:`BaseResponse` with a different + set of attributes for each thread. There is usually only one global + instance of this class (:data:`response`). Its attributes are used + to build the HTTP response at the end of the request/response cycle. + ''' + bind = BaseResponse.__init__ + _status_line = local_property() + _status_code = local_property() + _cookies = local_property() + _headers = local_property() + body = local_property() + + +Request = BaseRequest +Response = BaseResponse + + +class HTTPResponse(Response, BottleException): + def __init__(self, body='', status=None, headers=None, **more_headers): + super(HTTPResponse, self).__init__(body, status, headers, **more_headers) + + def apply(self, response): + response._status_code = self._status_code + response._status_line = self._status_line + response._headers = self._headers + response._cookies = self._cookies + response.body = self.body + + +class HTTPError(HTTPResponse): + default_status = 500 + def __init__(self, status=None, body=None, exception=None, traceback=None, + **options): + self.exception = exception + self.traceback = traceback + super(HTTPError, self).__init__(body, status, **options) + + + + + +############################################################################### +# Plugins ###################################################################### +############################################################################### + +class PluginError(BottleException): pass + + +class JSONPlugin(object): + name = 'json' + api = 2 + + def __init__(self, json_dumps=json_dumps): + self.json_dumps = json_dumps + + def apply(self, callback, route): + dumps = self.json_dumps + if not dumps: return callback + def wrapper(*a, **ka): + try: + rv = callback(*a, **ka) + except HTTPError: + rv = _e() + + if isinstance(rv, dict): + #Attempt to serialize, raises exception on failure + json_response = dumps(rv) + #Set content type only if serialization succesful + response.content_type = 'application/json' + return json_response + elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict): + rv.body = dumps(rv.body) + rv.content_type = 'application/json' + return rv + + return wrapper + + +class TemplatePlugin(object): + ''' This plugin applies the :func:`view` decorator to all routes with a + `template` config parameter. If the parameter is a tuple, the second + element must be a dict with additional options (e.g. `template_engine`) + or default variables for the template. ''' + name = 'template' + api = 2 + + def apply(self, callback, route): + conf = route.config.get('template') + if isinstance(conf, (tuple, list)) and len(conf) == 2: + return view(conf[0], **conf[1])(callback) + elif isinstance(conf, str): + return view(conf)(callback) + else: + return callback + + +#: Not a plugin, but part of the plugin API. TODO: Find a better place. +class _ImportRedirect(object): + def __init__(self, name, impmask): + ''' Create a virtual package that redirects imports (see PEP 302). ''' + self.name = name + self.impmask = impmask + self.module = sys.modules.setdefault(name, new_module(name)) + self.module.__dict__.update({'__file__': __file__, '__path__': [], + '__all__': [], '__loader__': self}) + sys.meta_path.append(self) + + def find_module(self, fullname, path=None): + if '.' not in fullname: return + packname = fullname.rsplit('.', 1)[0] + if packname != self.name: return + return self + + def load_module(self, fullname): + if fullname in sys.modules: return sys.modules[fullname] + modname = fullname.rsplit('.', 1)[1] + realname = self.impmask % modname + __import__(realname) + module = sys.modules[fullname] = sys.modules[realname] + setattr(self.module, modname, module) + module.__loader__ = self + return module + + + + + + +############################################################################### +# Common Utilities ############################################################# +############################################################################### + + +class MultiDict(DictMixin): + """ This dict stores multiple values per key, but behaves exactly like a + normal dict in that it returns only the newest value for any given key. + There are special methods available to access the full list of values. + """ + + def __init__(self, *a, **k): + self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items()) + + def __len__(self): return len(self.dict) + def __iter__(self): return iter(self.dict) + def __contains__(self, key): return key in self.dict + def __delitem__(self, key): del self.dict[key] + def __getitem__(self, key): return self.dict[key][-1] + def __setitem__(self, key, value): self.append(key, value) + def keys(self): return self.dict.keys() + + if py3k: + def values(self): return (v[-1] for v in self.dict.values()) + def items(self): return ((k, v[-1]) for k, v in self.dict.items()) + def allitems(self): + return ((k, v) for k, vl in self.dict.items() for v in vl) + iterkeys = keys + itervalues = values + iteritems = items + iterallitems = allitems + + else: + def values(self): return [v[-1] for v in self.dict.values()] + def items(self): return [(k, v[-1]) for k, v in self.dict.items()] + def iterkeys(self): return self.dict.iterkeys() + def itervalues(self): return (v[-1] for v in self.dict.itervalues()) + def iteritems(self): + return ((k, v[-1]) for k, v in self.dict.iteritems()) + def iterallitems(self): + return ((k, v) for k, vl in self.dict.iteritems() for v in vl) + def allitems(self): + return [(k, v) for k, vl in self.dict.iteritems() for v in vl] + + def get(self, key, default=None, index=-1, type=None): + ''' Return the most recent value for a key. + + :param default: The default value to be returned if the key is not + present or the type conversion fails. + :param index: An index for the list of available values. + :param type: If defined, this callable is used to cast the value + into a specific type. Exception are suppressed and result in + the default value to be returned. + ''' + try: + val = self.dict[key][index] + return type(val) if type else val + except Exception: + pass + return default + + def append(self, key, value): + ''' Add a new value to the list of values for this key. ''' + self.dict.setdefault(key, []).append(value) + + def replace(self, key, value): + ''' Replace the list of values with a single value. ''' + self.dict[key] = [value] + + def getall(self, key): + ''' Return a (possibly empty) list of values for a key. ''' + return self.dict.get(key) or [] + + #: Aliases for WTForms to mimic other multi-dict APIs (Django) + getone = get + getlist = getall + + +class FormsDict(MultiDict): + ''' This :class:`MultiDict` subclass is used to store request form data. + Additionally to the normal dict-like item access methods (which return + unmodified data as native strings), this container also supports + attribute-like access to its values. Attributes are automatically de- + or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing + attributes default to an empty string. ''' + + #: Encoding used for attribute values. + input_encoding = 'utf8' + #: If true (default), unicode strings are first encoded with `latin1` + #: and then decoded to match :attr:`input_encoding`. + recode_unicode = True + + def _fix(self, s, encoding=None): + if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI + return s.encode('latin1').decode(encoding or self.input_encoding) + elif isinstance(s, bytes): # Python 2 WSGI + return s.decode(encoding or self.input_encoding) + else: + return s + + def decode(self, encoding=None): + ''' Returns a copy with all keys and values de- or recoded to match + :attr:`input_encoding`. Some libraries (e.g. WTForms) want a + unicode dictionary. ''' + copy = FormsDict() + enc = copy.input_encoding = encoding or self.input_encoding + copy.recode_unicode = False + for key, value in self.allitems(): + copy.append(self._fix(key, enc), self._fix(value, enc)) + return copy + + def getunicode(self, name, default=None, encoding=None): + ''' Return the value as a unicode string, or the default. ''' + try: + return self._fix(self[name], encoding) + except (UnicodeError, KeyError): + return default + + def __getattr__(self, name, default=unicode()): + # Without this guard, pickle generates a cryptic TypeError: + if name.startswith('__') and name.endswith('__'): + return super(FormsDict, self).__getattr__(name) + return self.getunicode(name, default=default) + +class HeaderDict(MultiDict): + """ A case-insensitive version of :class:`MultiDict` that defaults to + replace the old value instead of appending it. """ + + def __init__(self, *a, **ka): + self.dict = {} + if a or ka: self.update(*a, **ka) + + def __contains__(self, key): return _hkey(key) in self.dict + def __delitem__(self, key): del self.dict[_hkey(key)] + def __getitem__(self, key): return self.dict[_hkey(key)][-1] + def __setitem__(self, key, value): self.dict[_hkey(key)] = [_hval(value)] + def append(self, key, value): self.dict.setdefault(_hkey(key), []).append(_hval(value)) + def replace(self, key, value): self.dict[_hkey(key)] = [_hval(value)] + def getall(self, key): return self.dict.get(_hkey(key)) or [] + def get(self, key, default=None, index=-1): + return MultiDict.get(self, _hkey(key), default, index) + def filter(self, names): + for name in (_hkey(n) for n in names): + if name in self.dict: + del self.dict[name] + + +class WSGIHeaderDict(DictMixin): + ''' This dict-like class wraps a WSGI environ dict and provides convenient + access to HTTP_* fields. Keys and values are native strings + (2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI + environment contains non-native string values, these are de- or encoded + using a lossless 'latin1' character set. + + The API will remain stable even on changes to the relevant PEPs. + Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one + that uses non-native strings.) + ''' + #: List of keys that do not have a ``HTTP_`` prefix. + cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') + + def __init__(self, environ): + self.environ = environ + + def _ekey(self, key): + ''' Translate header field name to CGI/WSGI environ key. ''' + key = key.replace('-','_').upper() + if key in self.cgikeys: + return key + return 'HTTP_' + key + + def raw(self, key, default=None): + ''' Return the header value as is (may be bytes or unicode). ''' + return self.environ.get(self._ekey(key), default) + + def __getitem__(self, key): + return tonat(self.environ[self._ekey(key)], 'latin1') + + def __setitem__(self, key, value): + raise TypeError("%s is read-only." % self.__class__) + + def __delitem__(self, key): + raise TypeError("%s is read-only." % self.__class__) + + def __iter__(self): + for key in self.environ: + if key[:5] == 'HTTP_': + yield key[5:].replace('_', '-').title() + elif key in self.cgikeys: + yield key.replace('_', '-').title() + + def keys(self): return [x for x in self] + def __len__(self): return len(self.keys()) + def __contains__(self, key): return self._ekey(key) in self.environ + + + +class ConfigDict(dict): + ''' A dict-like configuration storage with additional support for + namespaces, validators, meta-data, on_change listeners and more. + + This storage is optimized for fast read access. Retrieving a key + or using non-altering dict methods (e.g. `dict.get()`) has no overhead + compared to a native dict. + ''' + __slots__ = ('_meta', '_on_change') + + class Namespace(DictMixin): + + def __init__(self, config, namespace): + self._config = config + self._prefix = namespace + + def __getitem__(self, key): + depr('Accessing namespaces as dicts is discouraged. ' + 'Only use flat item access: ' + 'cfg["names"]["pace"]["key"] -> cfg["name.space.key"]') #0.12 + return self._config[self._prefix + '.' + key] + + def __setitem__(self, key, value): + self._config[self._prefix + '.' + key] = value + + def __delitem__(self, key): + del self._config[self._prefix + '.' + key] + + def __iter__(self): + ns_prefix = self._prefix + '.' + for key in self._config: + ns, dot, name = key.rpartition('.') + if ns == self._prefix and name: + yield name + + def keys(self): return [x for x in self] + def __len__(self): return len(self.keys()) + def __contains__(self, key): return self._prefix + '.' + key in self._config + def __repr__(self): return '<Config.Namespace %s.*>' % self._prefix + def __str__(self): return '<Config.Namespace %s.*>' % self._prefix + + # Deprecated ConfigDict features + def __getattr__(self, key): + depr('Attribute access is deprecated.') #0.12 + if key not in self and key[0].isupper(): + self[key] = ConfigDict.Namespace(self._config, self._prefix + '.' + key) + if key not in self and key.startswith('__'): + raise AttributeError(key) + return self.get(key) + + def __setattr__(self, key, value): + if key in ('_config', '_prefix'): + self.__dict__[key] = value + return + depr('Attribute assignment is deprecated.') #0.12 + if hasattr(DictMixin, key): + raise AttributeError('Read-only attribute.') + if key in self and self[key] and isinstance(self[key], self.__class__): + raise AttributeError('Non-empty namespace attribute.') + self[key] = value + + def __delattr__(self, key): + if key in self: + val = self.pop(key) + if isinstance(val, self.__class__): + prefix = key + '.' + for key in self: + if key.startswith(prefix): + del self[prefix+key] + + def __call__(self, *a, **ka): + depr('Calling ConfDict is deprecated. Use the update() method.') #0.12 + self.update(*a, **ka) + return self + + def __init__(self, *a, **ka): + self._meta = {} + self._on_change = lambda name, value: None + if a or ka: + depr('Constructor does no longer accept parameters.') #0.12 + self.update(*a, **ka) + + def load_config(self, filename): + ''' Load values from an *.ini style config file. + + If the config file contains sections, their names are used as + namespaces for the values within. The two special sections + ``DEFAULT`` and ``bottle`` refer to the root namespace (no prefix). + ''' + conf = ConfigParser() + conf.read(filename) + for section in conf.sections(): + for key, value in conf.items(section): + if section not in ('DEFAULT', 'bottle'): + key = section + '.' + key + self[key] = value + return self + + def load_dict(self, source, namespace='', make_namespaces=False): + ''' Import values from a dictionary structure. Nesting can be used to + represent namespaces. + + >>> ConfigDict().load_dict({'name': {'space': {'key': 'value'}}}) + {'name.space.key': 'value'} + ''' + stack = [(namespace, source)] + while stack: + prefix, source = stack.pop() + if not isinstance(source, dict): + raise TypeError('Source is not a dict (r)' % type(key)) + for key, value in source.items(): + if not isinstance(key, basestring): + raise TypeError('Key is not a string (%r)' % type(key)) + full_key = prefix + '.' + key if prefix else key + if isinstance(value, dict): + stack.append((full_key, value)) + if make_namespaces: + self[full_key] = self.Namespace(self, full_key) + else: + self[full_key] = value + return self + + def update(self, *a, **ka): + ''' If the first parameter is a string, all keys are prefixed with this + namespace. Apart from that it works just as the usual dict.update(). + Example: ``update('some.namespace', key='value')`` ''' + prefix = '' + if a and isinstance(a[0], basestring): + prefix = a[0].strip('.') + '.' + a = a[1:] + for key, value in dict(*a, **ka).items(): + self[prefix+key] = value + + def setdefault(self, key, value): + if key not in self: + self[key] = value + return self[key] + + def __setitem__(self, key, value): + if not isinstance(key, basestring): + raise TypeError('Key has type %r (not a string)' % type(key)) + + value = self.meta_get(key, 'filter', lambda x: x)(value) + if key in self and self[key] is value: + return + self._on_change(key, value) + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + + def clear(self): + for key in self: + del self[key] + + def meta_get(self, key, metafield, default=None): + ''' Return the value of a meta field for a key. ''' + return self._meta.get(key, {}).get(metafield, default) + + def meta_set(self, key, metafield, value): + ''' Set the meta field for a key to a new value. This triggers the + on-change handler for existing keys. ''' + self._meta.setdefault(key, {})[metafield] = value + if key in self: + self[key] = self[key] + + def meta_list(self, key): + ''' Return an iterable of meta field names defined for a key. ''' + return self._meta.get(key, {}).keys() + + # Deprecated ConfigDict features + def __getattr__(self, key): + depr('Attribute access is deprecated.') #0.12 + if key not in self and key[0].isupper(): + self[key] = self.Namespace(self, key) + if key not in self and key.startswith('__'): + raise AttributeError(key) + return self.get(key) + + def __setattr__(self, key, value): + if key in self.__slots__: + return dict.__setattr__(self, key, value) + depr('Attribute assignment is deprecated.') #0.12 + if hasattr(dict, key): + raise AttributeError('Read-only attribute.') + if key in self and self[key] and isinstance(self[key], self.Namespace): + raise AttributeError('Non-empty namespace attribute.') + self[key] = value + + def __delattr__(self, key): + if key in self: + val = self.pop(key) + if isinstance(val, self.Namespace): + prefix = key + '.' + for key in self: + if key.startswith(prefix): + del self[prefix+key] + + def __call__(self, *a, **ka): + depr('Calling ConfDict is deprecated. Use the update() method.') #0.12 + self.update(*a, **ka) + return self + + + +class AppStack(list): + """ A stack-like list. Calling it returns the head of the stack. """ + + def __call__(self): + """ Return the current default application. """ + return self[-1] + + def push(self, value=None): + """ Add a new :class:`Bottle` instance to the stack """ + if not isinstance(value, Bottle): + value = Bottle() + self.append(value) + return value + + +class WSGIFileWrapper(object): + + def __init__(self, fp, buffer_size=1024*64): + self.fp, self.buffer_size = fp, buffer_size + for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'): + if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) + + def __iter__(self): + buff, read = self.buffer_size, self.read + while True: + part = read(buff) + if not part: return + yield part + + +class _closeiter(object): + ''' This only exists to be able to attach a .close method to iterators that + do not support attribute assignment (most of itertools). ''' + + def __init__(self, iterator, close=None): + self.iterator = iterator + self.close_callbacks = makelist(close) + + def __iter__(self): + return iter(self.iterator) + + def close(self): + for func in self.close_callbacks: + func() + + +class ResourceManager(object): + ''' This class manages a list of search paths and helps to find and open + application-bound resources (files). + + :param base: default value for :meth:`add_path` calls. + :param opener: callable used to open resources. + :param cachemode: controls which lookups are cached. One of 'all', + 'found' or 'none'. + ''' + + def __init__(self, base='./', opener=open, cachemode='all'): + self.opener = open + self.base = base + self.cachemode = cachemode + + #: A list of search paths. See :meth:`add_path` for details. + self.path = [] + #: A cache for resolved paths. ``res.cache.clear()`` clears the cache. + self.cache = {} + + def add_path(self, path, base=None, index=None, create=False): + ''' Add a new path to the list of search paths. Return False if the + path does not exist. + + :param path: The new search path. Relative paths are turned into + an absolute and normalized form. If the path looks like a file + (not ending in `/`), the filename is stripped off. + :param base: Path used to absolutize relative search paths. + Defaults to :attr:`base` which defaults to ``os.getcwd()``. + :param index: Position within the list of search paths. Defaults + to last index (appends to the list). + + The `base` parameter makes it easy to reference files installed + along with a python module or package:: + + res.add_path('./resources/', __file__) + ''' + base = os.path.abspath(os.path.dirname(base or self.base)) + path = os.path.abspath(os.path.join(base, os.path.dirname(path))) + path += os.sep + if path in self.path: + self.path.remove(path) + if create and not os.path.isdir(path): + os.makedirs(path) + if index is None: + self.path.append(path) + else: + self.path.insert(index, path) + self.cache.clear() + return os.path.exists(path) + + def __iter__(self): + ''' Iterate over all existing files in all registered paths. ''' + search = self.path[:] + while search: + path = search.pop() + if not os.path.isdir(path): continue + for name in os.listdir(path): + full = os.path.join(path, name) + if os.path.isdir(full): search.append(full) + else: yield full + + def lookup(self, name): + ''' Search for a resource and return an absolute file path, or `None`. + + The :attr:`path` list is searched in order. The first match is + returend. Symlinks are followed. The result is cached to speed up + future lookups. ''' + if name not in self.cache or DEBUG: + for path in self.path: + fpath = os.path.join(path, name) + if os.path.isfile(fpath): + if self.cachemode in ('all', 'found'): + self.cache[name] = fpath + return fpath + if self.cachemode == 'all': + self.cache[name] = None + return self.cache[name] + + def open(self, name, mode='r', *args, **kwargs): + ''' Find a resource and return a file object, or raise IOError. ''' + fname = self.lookup(name) + if not fname: raise IOError("Resource %r not found." % name) + return self.opener(fname, mode=mode, *args, **kwargs) + + +class FileUpload(object): + + def __init__(self, fileobj, name, filename, headers=None): + ''' Wrapper for file uploads. ''' + #: Open file(-like) object (BytesIO buffer or temporary file) + self.file = fileobj + #: Name of the upload form field + self.name = name + #: Raw filename as sent by the client (may contain unsafe characters) + self.raw_filename = filename + #: A :class:`HeaderDict` with additional headers (e.g. content-type) + self.headers = HeaderDict(headers) if headers else HeaderDict() + + content_type = HeaderProperty('Content-Type') + content_length = HeaderProperty('Content-Length', reader=int, default=-1) + + def get_header(self, name, default=None): + """ Return the value of a header within the mulripart part. """ + return self.headers.get(name, default) + + @cached_property + def filename(self): + ''' Name of the file on the client file system, but normalized to ensure + file system compatibility. An empty filename is returned as 'empty'. + + Only ASCII letters, digits, dashes, underscores and dots are + allowed in the final filename. Accents are removed, if possible. + Whitespace is replaced by a single dash. Leading or tailing dots + or dashes are removed. The filename is limited to 255 characters. + ''' + fname = self.raw_filename + if not isinstance(fname, unicode): + fname = fname.decode('utf8', 'ignore') + fname = normalize('NFKD', fname).encode('ASCII', 'ignore').decode('ASCII') + fname = os.path.basename(fname.replace('\\', os.path.sep)) + fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip() + fname = re.sub(r'[-\s]+', '-', fname).strip('.-') + return fname[:255] or 'empty' + + def _copy_file(self, fp, chunk_size=2**16): + read, write, offset = self.file.read, fp.write, self.file.tell() + while 1: + buf = read(chunk_size) + if not buf: break + write(buf) + self.file.seek(offset) + + def save(self, destination, overwrite=False, chunk_size=2**16): + ''' Save file to disk or copy its content to an open file(-like) object. + If *destination* is a directory, :attr:`filename` is added to the + path. Existing files are not overwritten by default (IOError). + + :param destination: File path, directory or file(-like) object. + :param overwrite: If True, replace existing files. (default: False) + :param chunk_size: Bytes to read at a time. (default: 64kb) + ''' + if isinstance(destination, basestring): # Except file-likes here + if os.path.isdir(destination): + destination = os.path.join(destination, self.filename) + if not overwrite and os.path.exists(destination): + raise IOError('File exists.') + with open(destination, 'wb') as fp: + self._copy_file(fp, chunk_size) + else: + self._copy_file(destination, chunk_size) + + + + + + +############################################################################### +# Application Helper ########################################################### +############################################################################### + + +def abort(code=500, text='Unknown Error.'): + """ Aborts execution and causes a HTTP error. """ + raise HTTPError(code, text) + + +def redirect(url, code=None): + """ Aborts execution and causes a 303 or 302 redirect, depending on + the HTTP protocol version. """ + if not code: + code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 + res = response.copy(cls=HTTPResponse) + res.status = code + res.body = "" + res.set_header('Location', urljoin(request.url, url)) + raise res + + +def _file_iter_range(fp, offset, bytes, maxread=1024*1024): + ''' Yield chunks from a range in a file. No chunk is bigger than maxread.''' + fp.seek(offset) + while bytes > 0: + part = fp.read(min(bytes, maxread)) + if not part: break + bytes -= len(part) + yield part + + +def static_file(filename, root, mimetype='auto', download=False, charset='UTF-8'): + """ Open a file in a safe way and return :exc:`HTTPResponse` with status + code 200, 305, 403 or 404. The ``Content-Type``, ``Content-Encoding``, + ``Content-Length`` and ``Last-Modified`` headers are set if possible. + Special support for ``If-Modified-Since``, ``Range`` and ``HEAD`` + requests. + + :param filename: Name or path of the file to send. + :param root: Root path for file lookups. Should be an absolute directory + path. + :param mimetype: Defines the content-type header (default: guess from + file extension) + :param download: If True, ask the browser to open a `Save as...` dialog + instead of opening the file with the associated program. You can + specify a custom filename as a string. If not specified, the + original filename is used (default: False). + :param charset: The charset to use for files with a ``text/*`` + mime-type. (default: UTF-8) + """ + + root = os.path.abspath(root) + os.sep + filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) + headers = dict() + + if not filename.startswith(root): + return HTTPError(403, "Access denied.") + if not os.path.exists(filename) or not os.path.isfile(filename): + return HTTPError(404, "File does not exist.") + if not os.access(filename, os.R_OK): + return HTTPError(403, "You do not have permission to access this file.") + + if mimetype == 'auto': + mimetype, encoding = mimetypes.guess_type(filename) + if encoding: headers['Content-Encoding'] = encoding + + if mimetype: + if mimetype[:5] == 'text/' and charset and 'charset' not in mimetype: + mimetype += '; charset=%s' % charset + headers['Content-Type'] = mimetype + + if download: + download = os.path.basename(filename if download == True else download) + headers['Content-Disposition'] = 'attachment; filename="%s"' % download + + stats = os.stat(filename) + headers['Content-Length'] = clen = stats.st_size + lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) + headers['Last-Modified'] = lm + + ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') + if ims: + ims = parse_date(ims.split(";")[0].strip()) + if ims is not None and ims >= int(stats.st_mtime): + headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) + return HTTPResponse(status=304, **headers) + + body = '' if request.method == 'HEAD' else open(filename, 'rb') + + headers["Accept-Ranges"] = "bytes" + ranges = request.environ.get('HTTP_RANGE') + if 'HTTP_RANGE' in request.environ: + ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen)) + if not ranges: + return HTTPError(416, "Requested Range Not Satisfiable") + offset, end = ranges[0] + headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end-1, clen) + headers["Content-Length"] = str(end-offset) + if body: body = _file_iter_range(body, offset, end-offset) + return HTTPResponse(body, status=206, **headers) + return HTTPResponse(body, **headers) + + + + + + +############################################################################### +# HTTP Utilities and MISC (TODO) ############################################### +############################################################################### + + +def debug(mode=True): + """ Change the debug level. + There is only one debug level supported at the moment.""" + global DEBUG + if mode: warnings.simplefilter('default') + DEBUG = bool(mode) + +def http_date(value): + if isinstance(value, (datedate, datetime)): + value = value.utctimetuple() + elif isinstance(value, (int, float)): + value = time.gmtime(value) + if not isinstance(value, basestring): + value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) + return value + +def parse_date(ims): + """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ + try: + ts = email.utils.parsedate_tz(ims) + return time.mktime(ts[:8] + (0,)) - (ts[9] or 0) - time.timezone + except (TypeError, ValueError, IndexError, OverflowError): + return None + +def parse_auth(header): + """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" + try: + method, data = header.split(None, 1) + if method.lower() == 'basic': + user, pwd = touni(base64.b64decode(tob(data))).split(':',1) + return user, pwd + except (KeyError, ValueError): + return None + +def parse_range_header(header, maxlen=0): + ''' Yield (start, end) ranges parsed from a HTTP Range header. Skip + unsatisfiable ranges. The end index is non-inclusive.''' + if not header or header[:6] != 'bytes=': return + ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r] + for start, end in ranges: + try: + if not start: # bytes=-100 -> last 100 bytes + start, end = max(0, maxlen-int(end)), maxlen + elif not end: # bytes=100- -> all but the first 99 bytes + start, end = int(start), maxlen + else: # bytes=100-200 -> bytes 100-200 (inclusive) + start, end = int(start), min(int(end)+1, maxlen) + if 0 <= start < end <= maxlen: + yield start, end + except ValueError: + pass + +def _parse_qsl(qs): + r = [] + for pair in qs.split('&'): + if not pair: continue + nv = pair.split('=', 1) + if len(nv) != 2: nv.append('') + key = urlunquote(nv[0].replace('+', ' ')) + value = urlunquote(nv[1].replace('+', ' ')) + r.append((key, value)) + return r + +def _lscmp(a, b): + ''' Compares two strings in a cryptographically safe way: + Runtime is not affected by length of common prefix. ''' + return not sum(0 if x==y else 1 for x, y in zip(a, b)) and len(a) == len(b) + + +def cookie_encode(data, key): + ''' Encode and sign a pickle-able object. Return a (byte) string ''' + msg = base64.b64encode(pickle.dumps(data, -1)) + sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest()) + return tob('!') + sig + tob('?') + msg + + +def cookie_decode(data, key): + ''' Verify and decode an encoded string. Return an object or None.''' + data = tob(data) + if cookie_is_encoded(data): + sig, msg = data.split(tob('?'), 1) + if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())): + return pickle.loads(base64.b64decode(msg)) + return None + + +def cookie_is_encoded(data): + ''' Return True if the argument looks like a encoded cookie.''' + return bool(data.startswith(tob('!')) and tob('?') in data) + + +def html_escape(string): + ''' Escape HTML special characters ``&<>`` and quotes ``'"``. ''' + return string.replace('&','&').replace('<','<').replace('>','>')\ + .replace('"','"').replace("'",''') + + +def html_quote(string): + ''' Escape and quote a string to be used as an HTTP attribute.''' + return '"%s"' % html_escape(string).replace('\n',' ')\ + .replace('\r',' ').replace('\t','	') + + +def yieldroutes(func): + """ Return a generator for routes that match the signature (name, args) + of the func parameter. This may yield more than one route if the function + takes optional keyword arguments. The output is best described by example:: + + a() -> '/a' + b(x, y) -> '/b/<x>/<y>' + c(x, y=5) -> '/c/<x>' and '/c/<x>/<y>' + d(x=5, y=6) -> '/d' and '/d/<x>' and '/d/<x>/<y>' + """ + path = '/' + func.__name__.replace('__','/').lstrip('/') + spec = getargspec(func) + argc = len(spec[0]) - len(spec[3] or []) + path += ('/<%s>' * argc) % tuple(spec[0][:argc]) + yield path + for arg in spec[0][argc:]: + path += '/<%s>' % arg + yield path + + +def path_shift(script_name, path_info, shift=1): + ''' Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. + + :return: The modified paths. + :param script_name: The SCRIPT_NAME path. + :param script_name: The PATH_INFO path. + :param shift: The number of path fragments to shift. May be negative to + change the shift direction. (default: 1) + ''' + if shift == 0: return script_name, path_info + pathlist = path_info.strip('/').split('/') + scriptlist = script_name.strip('/').split('/') + if pathlist and pathlist[0] == '': pathlist = [] + if scriptlist and scriptlist[0] == '': scriptlist = [] + if shift > 0 and shift <= len(pathlist): + moved = pathlist[:shift] + scriptlist = scriptlist + moved + pathlist = pathlist[shift:] + elif shift < 0 and shift >= -len(scriptlist): + moved = scriptlist[shift:] + pathlist = moved + pathlist + scriptlist = scriptlist[:shift] + else: + empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO' + raise AssertionError("Cannot shift. Nothing left from %s" % empty) + new_script_name = '/' + '/'.join(scriptlist) + new_path_info = '/' + '/'.join(pathlist) + if path_info.endswith('/') and pathlist: new_path_info += '/' + return new_script_name, new_path_info + + +def auth_basic(check, realm="private", text="Access denied"): + ''' Callback decorator to require HTTP auth (basic). + TODO: Add route(check_auth=...) parameter. ''' + def decorator(func): + def wrapper(*a, **ka): + user, password = request.auth or (None, None) + if user is None or not check(user, password): + err = HTTPError(401, text) + err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm) + return err + return func(*a, **ka) + return wrapper + return decorator + + +# Shortcuts for common Bottle methods. +# They all refer to the current default application. + +def make_default_app_wrapper(name): + ''' Return a callable that relays calls to the current default app. ''' + @functools.wraps(getattr(Bottle, name)) + def wrapper(*a, **ka): + return getattr(app(), name)(*a, **ka) + return wrapper + +route = make_default_app_wrapper('route') +get = make_default_app_wrapper('get') +post = make_default_app_wrapper('post') +put = make_default_app_wrapper('put') +delete = make_default_app_wrapper('delete') +error = make_default_app_wrapper('error') +mount = make_default_app_wrapper('mount') +hook = make_default_app_wrapper('hook') +install = make_default_app_wrapper('install') +uninstall = make_default_app_wrapper('uninstall') +url = make_default_app_wrapper('get_url') + + + + + + + +############################################################################### +# Server Adapter ############################################################### +############################################################################### + + +class ServerAdapter(object): + quiet = False + def __init__(self, host='127.0.0.1', port=8080, **options): + self.options = options + self.host = host + self.port = int(port) + + def run(self, handler): # pragma: no cover + pass + + def __repr__(self): + args = ', '.join(['%s=%s'%(k,repr(v)) for k, v in self.options.items()]) + return "%s(%s)" % (self.__class__.__name__, args) + + +class CGIServer(ServerAdapter): + quiet = True + def run(self, handler): # pragma: no cover + from wsgiref.handlers import CGIHandler + def fixed_environ(environ, start_response): + environ.setdefault('PATH_INFO', '') + return handler(environ, start_response) + CGIHandler().run(fixed_environ) + + +class FlupFCGIServer(ServerAdapter): + def run(self, handler): # pragma: no cover + import flup.server.fcgi + self.options.setdefault('bindAddress', (self.host, self.port)) + flup.server.fcgi.WSGIServer(handler, **self.options).run() + + +class WSGIRefServer(ServerAdapter): + def run(self, app): # pragma: no cover + from wsgiref.simple_server import WSGIRequestHandler, WSGIServer + from wsgiref.simple_server import make_server + import socket + + class FixedHandler(WSGIRequestHandler): + def address_string(self): # Prevent reverse DNS lookups please. + return self.client_address[0] + def log_request(*args, **kw): + if not self.quiet: + return WSGIRequestHandler.log_request(*args, **kw) + + handler_cls = self.options.get('handler_class', FixedHandler) + server_cls = self.options.get('server_class', WSGIServer) + + if ':' in self.host: # Fix wsgiref for IPv6 addresses. + if getattr(server_cls, 'address_family') == socket.AF_INET: + class server_cls(server_cls): + address_family = socket.AF_INET6 + + srv = make_server(self.host, self.port, app, server_cls, handler_cls) + srv.serve_forever() + + +class CherryPyServer(ServerAdapter): + def run(self, handler): # pragma: no cover + from cherrypy import wsgiserver + self.options['bind_addr'] = (self.host, self.port) + self.options['wsgi_app'] = handler + + certfile = self.options.get('certfile') + if certfile: + del self.options['certfile'] + keyfile = self.options.get('keyfile') + if keyfile: + del self.options['keyfile'] + + server = wsgiserver.CherryPyWSGIServer(**self.options) + if certfile: + server.ssl_certificate = certfile + if keyfile: + server.ssl_private_key = keyfile + + try: + server.start() + finally: + server.stop() + + +class WaitressServer(ServerAdapter): + def run(self, handler): + from waitress import serve + serve(handler, host=self.host, port=self.port) + + +class PasteServer(ServerAdapter): + def run(self, handler): # pragma: no cover + from paste import httpserver + from paste.translogger import TransLogger + handler = TransLogger(handler, setup_console_handler=(not self.quiet)) + httpserver.serve(handler, host=self.host, port=str(self.port), + **self.options) + + +class MeinheldServer(ServerAdapter): + def run(self, handler): + from meinheld import server + server.listen((self.host, self.port)) + server.run(handler) + + +class FapwsServer(ServerAdapter): + """ Extremely fast webserver using libev. See http://www.fapws.org/ """ + def run(self, handler): # pragma: no cover + import fapws._evwsgi as evwsgi + from fapws import base, config + port = self.port + if float(config.SERVER_IDENT[-2:]) > 0.4: + # fapws3 silently changed its API in 0.5 + port = str(port) + evwsgi.start(self.host, port) + # fapws3 never releases the GIL. Complain upstream. I tried. No luck. + if 'BOTTLE_CHILD' in os.environ and not self.quiet: + _stderr("WARNING: Auto-reloading does not work with Fapws3.\n") + _stderr(" (Fapws3 breaks python thread support)\n") + evwsgi.set_base_module(base) + def app(environ, start_response): + environ['wsgi.multiprocess'] = False + return handler(environ, start_response) + evwsgi.wsgi_cb(('', app)) + evwsgi.run() + + +class TornadoServer(ServerAdapter): + """ The super hyped asynchronous server by facebook. Untested. """ + def run(self, handler): # pragma: no cover + import tornado.wsgi, tornado.httpserver, tornado.ioloop + container = tornado.wsgi.WSGIContainer(handler) + server = tornado.httpserver.HTTPServer(container) + server.listen(port=self.port,address=self.host) + tornado.ioloop.IOLoop.instance().start() + + +class AppEngineServer(ServerAdapter): + """ Adapter for Google App Engine. """ + quiet = True + def run(self, handler): + from google.appengine.ext.webapp import util + # A main() function in the handler script enables 'App Caching'. + # Lets makes sure it is there. This _really_ improves performance. + module = sys.modules.get('__main__') + if module and not hasattr(module, 'main'): + module.main = lambda: util.run_wsgi_app(handler) + util.run_wsgi_app(handler) + + +class TwistedServer(ServerAdapter): + """ Untested. """ + def run(self, handler): + from twisted.web import server, wsgi + from twisted.python.threadpool import ThreadPool + from twisted.internet import reactor + thread_pool = ThreadPool() + thread_pool.start() + reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop) + factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler)) + reactor.listenTCP(self.port, factory, interface=self.host) + reactor.run() + + +class DieselServer(ServerAdapter): + """ Untested. """ + def run(self, handler): + from diesel.protocols.wsgi import WSGIApplication + app = WSGIApplication(handler, port=self.port) + app.run() + + +class GeventServer(ServerAdapter): + """ Untested. Options: + + * `fast` (default: False) uses libevent's http server, but has some + issues: No streaming, no pipelining, no SSL. + * See gevent.wsgi.WSGIServer() documentation for more options. + """ + def run(self, handler): + from gevent import pywsgi, local + if not isinstance(threading.local(), local.local): + msg = "Bottle requires gevent.monkey.patch_all() (before import)" + raise RuntimeError(msg) + if self.options.pop('fast', None): + depr('The "fast" option has been deprecated and removed by Gevent.') + if self.quiet: + self.options['log'] = None + address = (self.host, self.port) + server = pywsgi.WSGIServer(address, handler, **self.options) + if 'BOTTLE_CHILD' in os.environ: + import signal + signal.signal(signal.SIGINT, lambda s, f: server.stop()) + server.serve_forever() + + +class GeventSocketIOServer(ServerAdapter): + def run(self,handler): + from socketio import server + address = (self.host, self.port) + server.SocketIOServer(address, handler, **self.options).serve_forever() + + +class GunicornServer(ServerAdapter): + """ Untested. See http://gunicorn.org/configure.html for options. """ + def run(self, handler): + from gunicorn.app.base import Application + + config = {'bind': "%s:%d" % (self.host, int(self.port))} + config.update(self.options) + + class GunicornApplication(Application): + def init(self, parser, opts, args): + return config + + def load(self): + return handler + + GunicornApplication().run() + + +class EventletServer(ServerAdapter): + """ Untested """ + def run(self, handler): + from eventlet import wsgi, listen + try: + wsgi.server(listen((self.host, self.port)), handler, + log_output=(not self.quiet)) + except TypeError: + # Fallback, if we have old version of eventlet + wsgi.server(listen((self.host, self.port)), handler) + + +class RocketServer(ServerAdapter): + """ Untested. """ + def run(self, handler): + from rocket import Rocket + server = Rocket((self.host, self.port), 'wsgi', { 'wsgi_app' : handler }) + server.start() + + +class BjoernServer(ServerAdapter): + """ Fast server written in C: https://github.com/jonashaag/bjoern """ + def run(self, handler): + from bjoern import run + run(handler, self.host, self.port) + + +class AutoServer(ServerAdapter): + """ Untested. """ + adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, WSGIRefServer] + def run(self, handler): + for sa in self.adapters: + try: + return sa(self.host, self.port, **self.options).run(handler) + except ImportError: + pass + +server_names = { + 'cgi': CGIServer, + 'flup': FlupFCGIServer, + 'wsgiref': WSGIRefServer, + 'waitress': WaitressServer, + 'cherrypy': CherryPyServer, + 'paste': PasteServer, + 'fapws3': FapwsServer, + 'tornado': TornadoServer, + 'gae': AppEngineServer, + 'twisted': TwistedServer, + 'diesel': DieselServer, + 'meinheld': MeinheldServer, + 'gunicorn': GunicornServer, + 'eventlet': EventletServer, + 'gevent': GeventServer, + 'geventSocketIO':GeventSocketIOServer, + 'rocket': RocketServer, + 'bjoern' : BjoernServer, + 'auto': AutoServer, +} + + + + + + +############################################################################### +# Application Control ########################################################## +############################################################################### + + +def load(target, **namespace): + """ Import a module or fetch an object from a module. + + * ``package.module`` returns `module` as a module object. + * ``pack.mod:name`` returns the module variable `name` from `pack.mod`. + * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result. + + The last form accepts not only function calls, but any type of + expression. Keyword arguments passed to this function are available as + local variables. Example: ``import_string('re:compile(x)', x='[a-z]')`` + """ + module, target = target.split(":", 1) if ':' in target else (target, None) + if module not in sys.modules: __import__(module) + if not target: return sys.modules[module] + if target.isalnum(): return getattr(sys.modules[module], target) + package_name = module.split('.')[0] + namespace[package_name] = sys.modules[package_name] + return eval('%s.%s' % (module, target), namespace) + + +def load_app(target): + """ Load a bottle application from a module and make sure that the import + does not affect the current default application, but returns a separate + application object. See :func:`load` for the target parameter. """ + global NORUN; NORUN, nr_old = True, NORUN + try: + tmp = default_app.push() # Create a new "default application" + rv = load(target) # Import the target module + return rv if callable(rv) else tmp + finally: + default_app.remove(tmp) # Remove the temporary added default application + NORUN = nr_old + +_debug = debug +def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, + interval=1, reloader=False, quiet=False, plugins=None, + debug=None, **kargs): + """ Start a server instance. This method blocks until the server terminates. + + :param app: WSGI application or target string supported by + :func:`load_app`. (default: :func:`default_app`) + :param server: Server adapter to use. See :data:`server_names` keys + for valid names or pass a :class:`ServerAdapter` subclass. + (default: `wsgiref`) + :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on + all interfaces including the external one. (default: 127.0.0.1) + :param port: Server port to bind to. Values below 1024 require root + privileges. (default: 8080) + :param reloader: Start auto-reloading server? (default: False) + :param interval: Auto-reloader interval in seconds (default: 1) + :param quiet: Suppress output to stdout and stderr? (default: False) + :param options: Options passed to the server adapter. + """ + if NORUN: return + if reloader and not os.environ.get('BOTTLE_CHILD'): + try: + lockfile = None + fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') + os.close(fd) # We only need this file to exist. We never write to it + while os.path.exists(lockfile): + args = [sys.executable] + sys.argv + environ = os.environ.copy() + environ['BOTTLE_CHILD'] = 'true' + environ['BOTTLE_LOCKFILE'] = lockfile + p = subprocess.Popen(args, env=environ) + while p.poll() is None: # Busy wait... + os.utime(lockfile, None) # I am alive! + time.sleep(interval) + if p.poll() != 3: + if os.path.exists(lockfile): os.unlink(lockfile) + sys.exit(p.poll()) + except KeyboardInterrupt: + pass + finally: + if os.path.exists(lockfile): + os.unlink(lockfile) + return + + try: + if debug is not None: _debug(debug) + app = app or default_app() + if isinstance(app, basestring): + app = load_app(app) + if not callable(app): + raise ValueError("Application is not callable: %r" % app) + + for plugin in plugins or []: + app.install(plugin) + + if server in server_names: + server = server_names.get(server) + if isinstance(server, basestring): + server = load(server) + if isinstance(server, type): + server = server(host=host, port=port, **kargs) + if not isinstance(server, ServerAdapter): + raise ValueError("Unknown or unsupported server: %r" % server) + + server.quiet = server.quiet or quiet + if not server.quiet: + _stderr("Bottle v%s server starting up (using %s)...\n" % (__version__, repr(server))) + _stderr("Listening on http://%s:%d/\n" % (server.host, server.port)) + _stderr("Hit Ctrl-C to quit.\n\n") + + if reloader: + lockfile = os.environ.get('BOTTLE_LOCKFILE') + bgcheck = FileCheckerThread(lockfile, interval) + with bgcheck: + server.run(app) + if bgcheck.status == 'reload': + sys.exit(3) + else: + server.run(app) + except KeyboardInterrupt: + pass + except (SystemExit, MemoryError): + raise + except: + if not reloader: raise + if not getattr(server, 'quiet', quiet): + print_exc() + time.sleep(interval) + sys.exit(3) + + + +class FileCheckerThread(threading.Thread): + ''' Interrupt main-thread as soon as a changed module file is detected, + the lockfile gets deleted or gets to old. ''' + + def __init__(self, lockfile, interval): + threading.Thread.__init__(self) + self.lockfile, self.interval = lockfile, interval + #: Is one of 'reload', 'error' or 'exit' + self.status = None + + def run(self): + exists = os.path.exists + mtime = lambda path: os.stat(path).st_mtime + files = dict() + + for module in list(sys.modules.values()): + path = getattr(module, '__file__', '') or '' + if path[-4:] in ('.pyo', '.pyc'): path = path[:-1] + if path and exists(path): files[path] = mtime(path) + + while not self.status: + if not exists(self.lockfile)\ + or mtime(self.lockfile) < time.time() - self.interval - 5: + self.status = 'error' + thread.interrupt_main() + for path, lmtime in list(files.items()): + if not exists(path) or mtime(path) > lmtime: + self.status = 'reload' + thread.interrupt_main() + break + time.sleep(self.interval) + + def __enter__(self): + self.start() + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self.status: self.status = 'exit' # silent exit + self.join() + return exc_type is not None and issubclass(exc_type, KeyboardInterrupt) + + + + + +############################################################################### +# Template Adapters ############################################################ +############################################################################### + + +class TemplateError(HTTPError): + def __init__(self, message): + HTTPError.__init__(self, 500, message) + + +class BaseTemplate(object): + """ Base class and minimal API for template adapters """ + extensions = ['tpl','html','thtml','stpl'] + settings = {} #used in prepare() + defaults = {} #used in render() + + def __init__(self, source=None, name=None, lookup=[], encoding='utf8', **settings): + """ Create a new template. + If the source parameter (str or buffer) is missing, the name argument + is used to guess a template filename. Subclasses can assume that + self.source and/or self.filename are set. Both are strings. + The lookup, encoding and settings parameters are stored as instance + variables. + The lookup parameter stores a list containing directory paths. + The encoding parameter should be used to decode byte strings or files. + The settings parameter contains a dict for engine-specific settings. + """ + self.name = name + self.source = source.read() if hasattr(source, 'read') else source + self.filename = source.filename if hasattr(source, 'filename') else None + self.lookup = [os.path.abspath(x) for x in lookup] + self.encoding = encoding + self.settings = self.settings.copy() # Copy from class variable + self.settings.update(settings) # Apply + if not self.source and self.name: + self.filename = self.search(self.name, self.lookup) + if not self.filename: + raise TemplateError('Template %s not found.' % repr(name)) + if not self.source and not self.filename: + raise TemplateError('No template specified.') + self.prepare(**self.settings) + + @classmethod + def search(cls, name, lookup=[]): + """ Search name in all directories specified in lookup. + First without, then with common extensions. Return first hit. """ + if not lookup: + depr('The template lookup path list should not be empty.') #0.12 + lookup = ['.'] + + if os.path.isabs(name) and os.path.isfile(name): + depr('Absolute template path names are deprecated.') #0.12 + return os.path.abspath(name) + + for spath in lookup: + spath = os.path.abspath(spath) + os.sep + fname = os.path.abspath(os.path.join(spath, name)) + if not fname.startswith(spath): continue + if os.path.isfile(fname): return fname + for ext in cls.extensions: + if os.path.isfile('%s.%s' % (fname, ext)): + return '%s.%s' % (fname, ext) + + @classmethod + def global_config(cls, key, *args): + ''' This reads or sets the global settings stored in class.settings. ''' + if args: + cls.settings = cls.settings.copy() # Make settings local to class + cls.settings[key] = args[0] + else: + return cls.settings[key] + + def prepare(self, **options): + """ Run preparations (parsing, caching, ...). + It should be possible to call this again to refresh a template or to + update settings. + """ + raise NotImplementedError + + def render(self, *args, **kwargs): + """ Render the template with the specified local variables and return + a single byte or unicode string. If it is a byte string, the encoding + must match self.encoding. This method must be thread-safe! + Local variables may be provided in dictionaries (args) + or directly, as keywords (kwargs). + """ + raise NotImplementedError + + +class MakoTemplate(BaseTemplate): + def prepare(self, **options): + from mako.template import Template + from mako.lookup import TemplateLookup + options.update({'input_encoding':self.encoding}) + options.setdefault('format_exceptions', bool(DEBUG)) + lookup = TemplateLookup(directories=self.lookup, **options) + if self.source: + self.tpl = Template(self.source, lookup=lookup, **options) + else: + self.tpl = Template(uri=self.name, filename=self.filename, lookup=lookup, **options) + + def render(self, *args, **kwargs): + for dictarg in args: kwargs.update(dictarg) + _defaults = self.defaults.copy() + _defaults.update(kwargs) + return self.tpl.render(**_defaults) + + +class CheetahTemplate(BaseTemplate): + def prepare(self, **options): + from Cheetah.Template import Template + self.context = threading.local() + self.context.vars = {} + options['searchList'] = [self.context.vars] + if self.source: + self.tpl = Template(source=self.source, **options) + else: + self.tpl = Template(file=self.filename, **options) + + def render(self, *args, **kwargs): + for dictarg in args: kwargs.update(dictarg) + self.context.vars.update(self.defaults) + self.context.vars.update(kwargs) + out = str(self.tpl) + self.context.vars.clear() + return out + + +class Jinja2Template(BaseTemplate): + def prepare(self, filters=None, tests=None, globals={}, **kwargs): + from jinja2 import Environment, FunctionLoader + if 'prefix' in kwargs: # TODO: to be removed after a while + raise RuntimeError('The keyword argument `prefix` has been removed. ' + 'Use the full jinja2 environment name line_statement_prefix instead.') + self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) + if filters: self.env.filters.update(filters) + if tests: self.env.tests.update(tests) + if globals: self.env.globals.update(globals) + if self.source: + self.tpl = self.env.from_string(self.source) + else: + self.tpl = self.env.get_template(self.filename) + + def render(self, *args, **kwargs): + for dictarg in args: kwargs.update(dictarg) + _defaults = self.defaults.copy() + _defaults.update(kwargs) + return self.tpl.render(**_defaults) + + def loader(self, name): + fname = self.search(name, self.lookup) + if not fname: return + with open(fname, "rb") as f: + return f.read().decode(self.encoding) + + +class SimpleTemplate(BaseTemplate): + + def prepare(self, escape_func=html_escape, noescape=False, syntax=None, **ka): + self.cache = {} + enc = self.encoding + self._str = lambda x: touni(x, enc) + self._escape = lambda x: escape_func(touni(x, enc)) + self.syntax = syntax + if noescape: + self._str, self._escape = self._escape, self._str + + @cached_property + def co(self): + return compile(self.code, self.filename or '<string>', 'exec') + + @cached_property + def code(self): + source = self.source + if not source: + with open(self.filename, 'rb') as f: + source = f.read() + try: + source, encoding = touni(source), 'utf8' + except UnicodeError: + depr('Template encodings other than utf8 are no longer supported.') #0.11 + source, encoding = touni(source, 'latin1'), 'latin1' + parser = StplParser(source, encoding=encoding, syntax=self.syntax) + code = parser.translate() + self.encoding = parser.encoding + return code + + def _rebase(self, _env, _name=None, **kwargs): + if _name is None: + depr('Rebase function called without arguments.' + ' You were probably looking for {{base}}?', True) #0.12 + _env['_rebase'] = (_name, kwargs) + + def _include(self, _env, _name=None, **kwargs): + if _name is None: + depr('Rebase function called without arguments.' + ' You were probably looking for {{base}}?', True) #0.12 + env = _env.copy() + env.update(kwargs) + if _name not in self.cache: + self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) + return self.cache[_name].execute(env['_stdout'], env) + + def execute(self, _stdout, kwargs): + env = self.defaults.copy() + env.update(kwargs) + env.update({'_stdout': _stdout, '_printlist': _stdout.extend, + 'include': functools.partial(self._include, env), + 'rebase': functools.partial(self._rebase, env), '_rebase': None, + '_str': self._str, '_escape': self._escape, 'get': env.get, + 'setdefault': env.setdefault, 'defined': env.__contains__ }) + eval(self.co, env) + if env.get('_rebase'): + subtpl, rargs = env.pop('_rebase') + rargs['base'] = ''.join(_stdout) #copy stdout + del _stdout[:] # clear stdout + return self._include(env, subtpl, **rargs) + return env + + def render(self, *args, **kwargs): + """ Render the template using keyword arguments as local variables. """ + env = {}; stdout = [] + for dictarg in args: env.update(dictarg) + env.update(kwargs) + self.execute(stdout, env) + return ''.join(stdout) + + +class StplSyntaxError(TemplateError): pass + + +class StplParser(object): + ''' Parser for stpl templates. ''' + _re_cache = {} #: Cache for compiled re patterns + # This huge pile of voodoo magic splits python code into 8 different tokens. + # 1: All kinds of python strings (trust me, it works) + _re_tok = '([urbURB]?(?:\'\'(?!\')|""(?!")|\'{6}|"{6}' \ + '|\'(?:[^\\\\\']|\\\\.)+?\'|"(?:[^\\\\"]|\\\\.)+?"' \ + '|\'{3}(?:[^\\\\]|\\\\.|\\n)+?\'{3}' \ + '|"{3}(?:[^\\\\]|\\\\.|\\n)+?"{3}))' + _re_inl = _re_tok.replace('|\\n','') # We re-use this string pattern later + # 2: Comments (until end of line, but not the newline itself) + _re_tok += '|(#.*)' + # 3,4: Open and close grouping tokens + _re_tok += '|([\\[\\{\\(])' + _re_tok += '|([\\]\\}\\)])' + # 5,6: Keywords that start or continue a python block (only start of line) + _re_tok += '|^([ \\t]*(?:if|for|while|with|try|def|class)\\b)' \ + '|^([ \\t]*(?:elif|else|except|finally)\\b)' + # 7: Our special 'end' keyword (but only if it stands alone) + _re_tok += '|((?:^|;)[ \\t]*end[ \\t]*(?=(?:%(block_close)s[ \\t]*)?\\r?$|;|#))' + # 8: A customizable end-of-code-block template token (only end of line) + _re_tok += '|(%(block_close)s[ \\t]*(?=\\r?$))' + # 9: And finally, a single newline. The 10th token is 'everything else' + _re_tok += '|(\\r?\\n)' + + # Match the start tokens of code areas in a template + _re_split = '(?m)^[ \t]*(\\\\?)((%(line_start)s)|(%(block_start)s))(%%?)' + # Match inline statements (may contain python strings) + _re_inl = '(?m)%%(inline_start)s((?:%s|[^\'"\n]*?)+)%%(inline_end)s' % _re_inl + _re_tok = '(?m)' + _re_tok + + default_syntax = '<% %> % {{ }}' + + def __init__(self, source, syntax=None, encoding='utf8'): + self.source, self.encoding = touni(source, encoding), encoding + self.set_syntax(syntax or self.default_syntax) + self.code_buffer, self.text_buffer = [], [] + self.lineno, self.offset = 1, 0 + self.indent, self.indent_mod = 0, 0 + self.paren_depth = 0 + + def get_syntax(self): + ''' Tokens as a space separated string (default: <% %> % {{ }}) ''' + return self._syntax + + def set_syntax(self, syntax): + self._syntax = syntax + self._tokens = syntax.split() + if not syntax in self._re_cache: + names = 'block_start block_close line_start inline_start inline_end' + etokens = map(re.escape, self._tokens) + pattern_vars = dict(zip(names.split(), etokens)) + patterns = (self._re_split, self._re_tok, self._re_inl) + patterns = [re.compile(p%pattern_vars) for p in patterns] + self._re_cache[syntax] = patterns + self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax] + + syntax = property(get_syntax, set_syntax) + + def translate(self): + if self.offset: raise RuntimeError('Parser is a one time instance.') + while True: + m = self.re_split.search(self.source[self.offset:]) + if m: + text = self.source[self.offset:self.offset+m.start()] + self.text_buffer.append(text) + self.offset += m.end() + if m.group(1): # New escape syntax + line, sep, _ = self.source[self.offset:].partition('\n') + self.text_buffer.append(m.group(2)+m.group(5)+line+sep) + self.offset += len(line+sep)+1 + continue + elif m.group(5): # Old escape syntax + depr('Escape code lines with a backslash.') #0.12 + line, sep, _ = self.source[self.offset:].partition('\n') + self.text_buffer.append(m.group(2)+line+sep) + self.offset += len(line+sep)+1 + continue + self.flush_text() + self.read_code(multiline=bool(m.group(4))) + else: break + self.text_buffer.append(self.source[self.offset:]) + self.flush_text() + return ''.join(self.code_buffer) + + def read_code(self, multiline): + code_line, comment = '', '' + while True: + m = self.re_tok.search(self.source[self.offset:]) + if not m: + code_line += self.source[self.offset:] + self.offset = len(self.source) + self.write_code(code_line.strip(), comment) + return + code_line += self.source[self.offset:self.offset+m.start()] + self.offset += m.end() + _str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups() + if (code_line or self.paren_depth > 0) and (_blk1 or _blk2): # a if b else c + code_line += _blk1 or _blk2 + continue + if _str: # Python string + code_line += _str + elif _com: # Python comment (up to EOL) + comment = _com + if multiline and _com.strip().endswith(self._tokens[1]): + multiline = False # Allow end-of-block in comments + elif _po: # open parenthesis + self.paren_depth += 1 + code_line += _po + elif _pc: # close parenthesis + if self.paren_depth > 0: + # we could check for matching parentheses here, but it's + # easier to leave that to python - just check counts + self.paren_depth -= 1 + code_line += _pc + elif _blk1: # Start-block keyword (if/for/while/def/try/...) + code_line, self.indent_mod = _blk1, -1 + self.indent += 1 + elif _blk2: # Continue-block keyword (else/elif/except/...) + code_line, self.indent_mod = _blk2, -1 + elif _end: # The non-standard 'end'-keyword (ends a block) + self.indent -= 1 + elif _cend: # The end-code-block template token (usually '%>') + if multiline: multiline = False + else: code_line += _cend + else: # \n + self.write_code(code_line.strip(), comment) + self.lineno += 1 + code_line, comment, self.indent_mod = '', '', 0 + if not multiline: + break + + def flush_text(self): + text = ''.join(self.text_buffer) + del self.text_buffer[:] + if not text: return + parts, pos, nl = [], 0, '\\\n'+' '*self.indent + for m in self.re_inl.finditer(text): + prefix, pos = text[pos:m.start()], m.end() + if prefix: + parts.append(nl.join(map(repr, prefix.splitlines(True)))) + if prefix.endswith('\n'): parts[-1] += nl + parts.append(self.process_inline(m.group(1).strip())) + if pos < len(text): + prefix = text[pos:] + lines = prefix.splitlines(True) + if lines[-1].endswith('\\\\\n'): lines[-1] = lines[-1][:-3] + elif lines[-1].endswith('\\\\\r\n'): lines[-1] = lines[-1][:-4] + parts.append(nl.join(map(repr, lines))) + code = '_printlist((%s,))' % ', '.join(parts) + self.lineno += code.count('\n')+1 + self.write_code(code) + + def process_inline(self, chunk): + if chunk[0] == '!': return '_str(%s)' % chunk[1:] + return '_escape(%s)' % chunk + + def write_code(self, line, comment=''): + line, comment = self.fix_backward_compatibility(line, comment) + code = ' ' * (self.indent+self.indent_mod) + code += line.lstrip() + comment + '\n' + self.code_buffer.append(code) + + def fix_backward_compatibility(self, line, comment): + parts = line.strip().split(None, 2) + if parts and parts[0] in ('include', 'rebase'): + depr('The include and rebase keywords are functions now.') #0.12 + if len(parts) == 1: return "_printlist([base])", comment + elif len(parts) == 2: return "_=%s(%r)" % tuple(parts), comment + else: return "_=%s(%r, %s)" % tuple(parts), comment + if self.lineno <= 2 and not line.strip() and 'coding' in comment: + m = re.match(r"#.*coding[:=]\s*([-\w.]+)", comment) + if m: + depr('PEP263 encoding strings in templates are deprecated.') #0.12 + enc = m.group(1) + self.source = self.source.encode(self.encoding).decode(enc) + self.encoding = enc + return line, comment.replace('coding','coding*') + return line, comment + + +def template(*args, **kwargs): + ''' + Get a rendered template as a string iterator. + You can use a name, a filename or a template string as first parameter. + Template rendering arguments can be passed as dictionaries + or directly (as keyword arguments). + ''' + tpl = args[0] if args else None + adapter = kwargs.pop('template_adapter', SimpleTemplate) + lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) + tplid = (id(lookup), tpl) + if tplid not in TEMPLATES or DEBUG: + settings = kwargs.pop('template_settings', {}) + if isinstance(tpl, adapter): + TEMPLATES[tplid] = tpl + if settings: TEMPLATES[tplid].prepare(**settings) + elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: + TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings) + else: + TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) + if not TEMPLATES[tplid]: + abort(500, 'Template (%s) not found' % tpl) + for dictarg in args[1:]: kwargs.update(dictarg) + return TEMPLATES[tplid].render(kwargs) + +mako_template = functools.partial(template, template_adapter=MakoTemplate) +cheetah_template = functools.partial(template, template_adapter=CheetahTemplate) +jinja2_template = functools.partial(template, template_adapter=Jinja2Template) + + +def view(tpl_name, **defaults): + ''' Decorator: renders a template for a handler. + The handler can control its behavior like that: + + - return a dict of template vars to fill out the template + - return something other than a dict and the view decorator will not + process the template, but return the handler result as is. + This includes returning a HTTPResponse(dict) to get, + for instance, JSON with autojson or other castfilters. + ''' + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if isinstance(result, (dict, DictMixin)): + tplvars = defaults.copy() + tplvars.update(result) + return template(tpl_name, **tplvars) + elif result is None: + return template(tpl_name, defaults) + return result + return wrapper + return decorator + +mako_view = functools.partial(view, template_adapter=MakoTemplate) +cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) +jinja2_view = functools.partial(view, template_adapter=Jinja2Template) + + + + + + +############################################################################### +# Constants and Globals ######################################################## +############################################################################### + + +TEMPLATE_PATH = ['./', './views/'] +TEMPLATES = {} +DEBUG = False +NORUN = False # If set, run() does nothing. Used by load_app() + +#: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') +HTTP_CODES = httplib.responses +HTTP_CODES[418] = "I'm a teapot" # RFC 2324 +HTTP_CODES[422] = "Unprocessable Entity" # RFC 4918 +HTTP_CODES[428] = "Precondition Required" +HTTP_CODES[429] = "Too Many Requests" +HTTP_CODES[431] = "Request Header Fields Too Large" +HTTP_CODES[511] = "Network Authentication Required" +_HTTP_STATUS_LINES = dict((k, '%d %s'%(k,v)) for (k,v) in HTTP_CODES.items()) + +#: The default template used for error pages. Override with @error() +ERROR_PAGE_TEMPLATE = """ +%%try: + %%from %s import DEBUG, HTTP_CODES, request, touni + <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> + <html> + <head> + <title>Error: {{e.status}}</title> + <style type="text/css"> + html {background-color: #eee; font-family: sans;} + body {background-color: #fff; border: 1px solid #ddd; + padding: 15px; margin: 15px;} + pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;} + </style> + </head> + <body> + <h1>Error: {{e.status}}</h1> + <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt> + caused an error:</p> + <pre>{{e.body}}</pre> + %%if DEBUG and e.exception: + <h2>Exception:</h2> + <pre>{{repr(e.exception)}}</pre> + %%end + %%if DEBUG and e.traceback: + <h2>Traceback:</h2> + <pre>{{e.traceback}}</pre> + %%end + </body> + </html> +%%except ImportError: + <b>ImportError:</b> Could not generate the error page. Please add bottle to + the import path. +%%end +""" % __name__ + +#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a +#: request callback, this instance always refers to the *current* request +#: (even on a multithreaded server). +request = LocalRequest() + +#: A thread-safe instance of :class:`LocalResponse`. It is used to change the +#: HTTP response for the *current* request. +response = LocalResponse() + +#: A thread-safe namespace. Not used by Bottle. +local = threading.local() + +# Initialize app stack (create first empty Bottle app) +# BC: 0.6.4 and needed for run() +app = default_app = AppStack() +app.push() + +#: A virtual package that redirects import statements. +#: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. +ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else __name__+".ext", 'bottle_%s').module + +if __name__ == '__main__': + opt, args, parser = _cmd_options, _cmd_args, _cmd_parser + if opt.version: + _stdout('Bottle %s\n'%__version__) + sys.exit(0) + if not args: + parser.print_help() + _stderr('\nError: No application specified.\n') + sys.exit(1) + + sys.path.insert(0, '.') + sys.modules.setdefault('bottle', sys.modules['__main__']) + + host, port = (opt.bind or 'localhost'), 8080 + if ':' in host and host.rfind(']') < host.rfind(':'): + host, port = host.rsplit(':', 1) + host = host.strip('[]') + + run(args[0], host=host, port=int(port), server=opt.server, + reloader=opt.reload, plugins=opt.plugin, debug=opt.debug) + + + + +# THE END diff --git a/venv/lib/python3.9/site-packages/pympler/util/compat.py b/venv/lib/python3.9/site-packages/pympler/util/compat.py new file mode 100644 index 00000000..8065f3b2 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/util/compat.py @@ -0,0 +1,23 @@ +""" +Compatibility layer to allow Pympler being used from different Python versions. +""" + +from typing import Any, Iterable + +try: + import tkinter +except ImportError: + tkinter = None # type: ignore + + +# Helper functions + +def object_in_list(obj: Any, l: Iterable) -> bool: + """Returns True if object o is in list. + + Required compatibility function to handle WeakSet objects. + """ + for o in l: + if o is obj: + return True + return False diff --git a/venv/lib/python3.9/site-packages/pympler/util/stringutils.py b/venv/lib/python3.9/site-packages/pympler/util/stringutils.py new file mode 100644 index 00000000..7b09e8c3 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/util/stringutils.py @@ -0,0 +1,77 @@ +""" +String utility functions. +""" + +from typing import Any, Optional, Union + + +def safe_repr(obj: Any, clip: Optional[int] = None) -> str: + """ + Convert object to string representation, yielding the same result a `repr` + but catches all exceptions and returns 'N/A' instead of raising the + exception. Strings may be truncated by providing `clip`. + + >>> safe_repr(42) + '42' + >>> safe_repr('Clipped text', clip=8) + 'Clip..xt' + >>> safe_repr([1,2,3,4], clip=8) + '[1,2..4]' + """ + try: + s = repr(obj) + if not clip or len(s) <= clip: + return s + else: + return s[:clip - 4] + '..' + s[-2:] + except: + return 'N/A' + + +def trunc(obj: str, max: int, left: bool = False) -> str: + """ + Convert `obj` to string, eliminate newlines and truncate the string to + `max` characters. If there are more characters in the string add ``...`` to + the string. With `left=True`, the string can be truncated at the beginning. + + @note: Does not catch exceptions when converting `obj` to string with + `str`. + + >>> trunc('This is a long text.', 8) + This ... + >>> trunc('This is a long text.', 8, left=True) + ...text. + """ + s = str(obj) + s = s.replace('\n', '|') + if len(s) > max: + if left: + return '...' + s[len(s) - max + 3:] + else: + return s[:(max - 3)] + '...' + else: + return s + + +def pp(i: Union[int, float], base: int = 1024) -> str: + """ + Pretty-print the integer `i` as a human-readable size representation. + """ + degree = 0 + pattern = "%4d %s" + while i > base: + pattern = "%7.2f %s" + i = i / float(base) + degree += 1 + scales = ['B', 'KB', 'MB', 'GB', 'TB', 'EB'] + return pattern % (i, scales[degree]) + + +def pp_timestamp(t: Optional[float]) -> str: + """ + Get a friendly timestamp represented as a string. + """ + if t is None: + return '' + h, m, s = int(t / 3600), int(t / 60 % 60), t % 60 + return "%02d:%02d:%05.2f" % (h, m, s) diff --git a/venv/lib/python3.9/site-packages/pympler/web.py b/venv/lib/python3.9/site-packages/pympler/web.py new file mode 100644 index 00000000..12888ab0 --- /dev/null +++ b/venv/lib/python3.9/site-packages/pympler/web.py @@ -0,0 +1,346 @@ +""" +This module provides a web-based memory profiling interface. The Pympler web +frontend exposes process information, tracker statistics, and garbage graphs. +The web frontend uses `Bottle <http://bottlepy.org>`_, a lightweight Python +web framework. Bottle is packaged with Pympler. + +The web server can be invoked almost as easily as setting a breakpoint using +*pdb*:: + + from pympler.web import start_profiler + start_profiler() + +Calling ``start_profiler`` suspends the current thread and executes the Pympler +web server, exposing profiling data and various facilities of the Pympler +library via a graphic interface. +""" + +import sys +import os +import threading + +from inspect import getouterframes +from json import dumps +from shutil import rmtree +from tempfile import mkdtemp +from threading import Thread +from weakref import WeakValueDictionary +from wsgiref.simple_server import make_server + +from pympler import asizeof +from pympler.garbagegraph import GarbageGraph +from pympler.process import get_current_threads, ProcessMemoryInfo + +from pympler.util.stringutils import safe_repr + +# Prefer the installed version of bottle.py. If bottle.py is not installed +# fallback to the vendored version. +try: + import bottle +except ImportError: + from pympler.util import bottle + + +class ServerState(threading.local): + """ + Represents the state of a running server. Needs to be thread local so + multiple servers can be started in different threads without interfering + with each other. + + Cache internal structures (garbage graphs, tracker statistics). + """ + def __init__(self): + self.server = None + self.stats = None + self.garbage_graphs = None + self.id2ref = WeakValueDictionary() + self.id2obj = dict() + + def clear_cache(self): + self.garbage_graphs = None + + +server = ServerState() + + +def get_ref(obj): + """ + Get string reference to object. Stores a weak reference in a dictionary + using the object's id as the key. If the object cannot be weakly + referenced (e.g. dictionaries, frame objects), store a strong references + in a classic dictionary. + + Returns the object's id as a string. + """ + oid = id(obj) + try: + server.id2ref[oid] = obj + except TypeError: + server.id2obj[oid] = obj + return str(oid) + + +def get_obj(ref): + """Get object from string reference.""" + oid = int(ref) + return server.id2ref.get(oid) or server.id2obj[oid] + + +pympler_path = os.path.dirname(os.path.abspath(__file__)) +static_files = os.path.join(pympler_path, 'templates') + +bottle.TEMPLATE_PATH.append(static_files) + + +@bottle.route('/') +@bottle.view('index') +def root(): + """Get overview.""" + pmi = ProcessMemoryInfo() + return dict(processinfo=pmi) + + +@bottle.route('/process') +@bottle.view('process') +def process(): + """Get process overview.""" + pmi = ProcessMemoryInfo() + threads = get_current_threads() + return dict(info=pmi, threads=threads) + + +@bottle.route('/tracker') +@bottle.view('tracker') +def tracker_index(): + """Get tracker overview.""" + stats = server.stats + if stats and stats.snapshots: + stats.annotate() + timeseries = [] + for cls in stats.tracked_classes: + series = [] + for snapshot in stats.snapshots: + series.append(snapshot.classes.get(cls, {}).get('sum', 0)) + timeseries.append((cls, series)) + + series = [s.overhead for s in stats.snapshots] + timeseries.append(("Profiling overhead", series)) + + if stats.snapshots[0].system_total.data_segment: + # Assume tracked data resides in the data segment + series = [s.system_total.data_segment - s.tracked_total - s.overhead + for s in stats.snapshots] + timeseries.append(("Data segment", series)) + series = [s.system_total.code_segment for s in stats.snapshots] + timeseries.append(("Code segment", series)) + series = [s.system_total.stack_segment for s in stats.snapshots] + timeseries.append(("Stack segment", series)) + series = [s.system_total.shared_segment for s in stats.snapshots] + timeseries.append(("Shared memory", series)) + else: + series = [s.total - s.tracked_total - s.overhead + for s in stats.snapshots] + timeseries.append(("Other", series)) + timeseries = [dict(label=label, data=list(enumerate(data))) + for label, data in timeseries] + return dict(snapshots=stats.snapshots, timeseries=dumps(timeseries)) + else: + return dict(snapshots=[]) + + +@bottle.route('/tracker/class/<clsname>') +@bottle.view('tracker_class') +def tracker_class(clsname): + """Get class instance details.""" + stats = server.stats + if not stats: + bottle.redirect('/tracker') + stats.annotate() + return dict(stats=stats, clsname=clsname) + + +@bottle.route('/refresh') +def refresh(): + """Clear all cached information.""" + server.clear_cache() + bottle.redirect('/') + + +@bottle.route('/traceback/<threadid>') +@bottle.view('stacktrace') +def get_traceback(threadid): + threadid = int(threadid) + frames = sys._current_frames() + if threadid in frames: + frame = frames[threadid] + stack = getouterframes(frame, 5) + stack.reverse() + stack = [(get_ref(f[0].f_locals),) + f[1:] for f in stack] + else: + stack = [] + return dict(stack=stack, threadid=threadid) + + +@bottle.route('/objects/<oid>') +@bottle.view('referents') +def get_obj_referents(oid): + referents = {} + obj = get_obj(oid) + if type(obj) is dict: + named_objects = asizeof.named_refs(obj) + else: + refs = asizeof._getreferents(obj) + named_objects = [(repr(type(x)), x) for x in refs] + for name, o in named_objects: + referents[name] = (get_ref(o), type(o).__name__, + safe_repr(o, clip=48), asizeof.asizeof(o)) + return dict(referents=referents) + + +@bottle.route('/static/<filename>') +def static_file(filename): + """Get static files (CSS-files).""" + return bottle.static_file(filename, root=static_files) + + +def _compute_garbage_graphs(): + """ + Retrieve garbage graph objects from cache, compute if cache is cold. + """ + if server.garbage_graphs is None: + server.garbage_graphs = GarbageGraph().split_and_sort() + return server.garbage_graphs + + +@bottle.route('/garbage') +@bottle.view('garbage_index') +def garbage_index(): + """Get garbage overview.""" + garbage_graphs = _compute_garbage_graphs() + return dict(graphs=garbage_graphs) + + +@bottle.route('/garbage/<index:int>') +@bottle.view('garbage') +def garbage_cycle(index): + """Get reference cycle details.""" + graph = _compute_garbage_graphs()[int(index)] + graph.reduce_to_cycles() + objects = graph.metadata + objects.sort(key=lambda x: -x.size) + return dict(objects=objects, index=index) + + +def _get_graph(graph, filename): + """Retrieve or render a graph.""" + try: + rendered = graph.rendered_file + except AttributeError: + try: + graph.render(os.path.join(server.tmpdir, filename), format='png') + rendered = filename + except OSError: + rendered = None + graph.rendered_file = rendered + return rendered + + +@bottle.route('/garbage/graph/<index:int>') +def garbage_graph(index): + """Get graph representation of reference cycle.""" + graph = _compute_garbage_graphs()[int(index)] + reduce_graph = bottle.request.GET.get('reduce', '') + if reduce_graph: + graph = graph.reduce_to_cycles() + if not graph: + return None + filename = 'garbage%so%s.png' % (index, reduce_graph) + rendered_file = _get_graph(graph, filename) + if rendered_file: + return bottle.static_file(rendered_file, root=server.tmpdir) + else: + return None + + +@bottle.route('/help') +def show_documentation(): + """Redirect to online documentation.""" + bottle.redirect('https://pympler.readthedocs.io/en/latest/') + + +class PymplerServer(bottle.ServerAdapter): + """Simple WSGI server.""" + def run(self, handler): + self.server = make_server(self.host, self.port, handler) + self.server.serve_forever() + + +def start_profiler(host='localhost', port=8090, tracker=None, stats=None, + debug=False, **kwargs): + """ + Start the web server to show profiling data. The function suspends the + Python application (the current thread) until the web server is stopped. + + The only way to stop the server is to signal the running thread, e.g. press + Ctrl+C in the console. If this isn't feasible for your application use + `start_in_background` instead. + + During the execution of the web server, profiling data is (lazily) cached + to improve performance. For example, garbage graphs are rendered when the + garbage profiling data is requested and are simply retransmitted upon later + requests. + + The web server can display profiling data from previously taken snapshots + when `tracker` or `stats` is specified. The former is useful for profiling + a running application, the latter for off-line analysis. Requires existing + snapshots taken with + :py:meth:`~pympler.classtracker.ClassTracker.create_snapshot` or + :py:meth:`~pympler.classtracker.ClassTracker.start_periodic_snapshots`. + + :param host: the host where the server shall run, default is localhost + :param port: server listens on the specified port, default is 8090 to allow + coexistance with common web applications + :param tracker: `ClassTracker` instance, browse profiling data (on-line + analysis) + :param stats: `Stats` instance, analyze `ClassTracker` profiling dumps + (useful for off-line analysis) + """ + if tracker and not stats: + server.stats = tracker.stats + else: + server.stats = stats + try: + server.tmpdir = mkdtemp(prefix='pympler') + server.server = PymplerServer(host=host, port=port, **kwargs) + bottle.debug(debug) + bottle.run(server=server.server) + finally: + rmtree(server.tmpdir) + + +class ProfilerThread(Thread): + """Encapsulates a thread to run the web server.""" + def __init__(self, group=None, target=None, name='Pympler web frontend', + **kwargs): + super(ProfilerThread, self).__init__(group=group, + target=target, + name=name) + self.kwargs = kwargs + self.daemon = True + + def run(self): + start_profiler(**self.kwargs) + + +def start_in_background(**kwargs): + """ + Start the web server in the background. A new thread is created which + serves the profiling interface without suspending the current application. + + For the documentation of the parameters see `start_profiler`. + + Returns the created thread object. + """ + thread = ProfilerThread(**kwargs) + thread.start() + return thread |