Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backlog #81

Closed
wants to merge 40 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
54509e5
add backlog predicate
Dan-Ailenei Mar 23, 2020
a03ed85
add filter to backlog
Dan-Ailenei Mar 25, 2020
f40485e
add tests for backlog size with filter
Dan-Ailenei Mar 26, 2020
76786c9
Add two integration tests for the backlog.
ionelmc Mar 27, 2020
24e9d19
Add a cleanup api and rework the backlog. Remove self.called.
ionelmc Mar 27, 2020
4e4b601
Move the augmented sample7 to a new file and fix various test breakage.
ionelmc Mar 28, 2020
98888d0
More fixes and cleanup. Move the filter method inside the class.
ionelmc Mar 28, 2020
b6ffdce
A bit more cleanup.
ionelmc Mar 28, 2020
4f6b81e
Add two more tests and fix various sideffects in the suite.
ionelmc Mar 28, 2020
44ab0ec
Styling.
ionelmc Mar 28, 2020
107e61b
A bunch more fixes to the evil tracer to emulate the real thing more …
ionelmc Mar 28, 2020
34734de
A big load of Backlog fixes.
ionelmc Mar 29, 2020
3bf7b3f
Vendor colorama cause I can't be bothered to figure out virtualenv is…
ionelmc Mar 29, 2020
b13df3b
create first cython version of backlog
Dan-Ailenei Apr 26, 2020
33df824
Add a comment.
ionelmc May 23, 2020
85b5dc5
Another comment.
ionelmc May 23, 2020
b3bef94
Add missing entry to __all__.
ionelmc May 23, 2020
1ca7ac1
Temp.
ionelmc May 23, 2020
8774c10
add cython implementation
Dan-Ailenei May 23, 2020
a3996ee
Merge branch 'backlog' of https://github.com/Dan-Ailenei/python-hunte…
Dan-Ailenei May 23, 2020
5b255ed
Add a comment.
ionelmc May 23, 2020
665ac2f
Another comment.
ionelmc May 23, 2020
7234623
Add missing entry to __all__.
ionelmc May 23, 2020
bb522c9
Temp.
ionelmc May 23, 2020
c1e8d80
remove unwanted imports
Dan-Ailenei May 23, 2020
7a873be
Merge branch 'backlog' of https://github.com/Dan-Ailenei/python-hunte…
Dan-Ailenei May 23, 2020
8bef2dc
remove unwanted import
Dan-Ailenei May 23, 2020
822e6fd
Replace set_frame workaround with proper interfacing.
ionelmc May 24, 2020
d1ef8be
Upgrade cython.
ionelmc May 24, 2020
a2bdb6a
Make sure right types are used in more situations.
ionelmc May 24, 2020
16ab07b
WIP borken.
ionelmc May 24, 2020
9fd4dd7
make detach function cpdef and fix cython integration tests
Dan-Ailenei May 24, 2020
225388d
Make both cpdef (tests don't show significant performance difference …
ionelmc May 24, 2020
067115e
A bit of cleanup before debugging.
ionelmc May 25, 2020
d31f1b3
Move at the end and apply some fixups.
ionelmc May 25, 2020
fa35263
Fix quoting (the rest of the project is sq).
ionelmc May 25, 2020
23e6da7
Implement the test EvilTracer helper in cython and fix a bunch of tes…
ionelmc May 25, 2020
5581cbe
Looks like setuptools-scm broke some stuff. Ref https://github.com/py…
ionelmc May 25, 2020
5f23744
Cleanup.
ionelmc May 25, 2020
b265dac
Use any python.
ionelmc May 25, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 49 additions & 22 deletions src/hunter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,28 @@
from .actions import Manhole
from .actions import VarsPrinter
from .actions import VarsSnooper

try:
if os.environ.get("PUREPYTHONHUNTER"):
raise ImportError("Cython speedups are disabled.")

from ._event import Event
from ._predicates import And as _And
from ._predicates import From as _From
from ._predicates import Not as _Not
from ._predicates import Or as _Or
from ._predicates import When
from ._predicates import Query
from ._tracer import Tracer
except ImportError:
from .event import Event # noqa
from .predicates import And as _And
from .predicates import From as _From
from .predicates import Not as _Not
from .predicates import Or as _Or
from .predicates import When
from .predicates import Query
from .tracer import Tracer
# try:
# if os.environ.get("PUREPYTHONHUNTER"):
# raise ImportError("Cython speedups are disabled.")
#
# from ._event import Event
# from ._predicates import And as _And
# from ._predicates import From as _From
# from ._predicates import Not as _Not
# from ._predicates import Or as _Or
# from ._predicates import When
# from ._predicates import Query
# from ._tracer import Tracer
# except ImportError:
from .event import Event # noqa
from .predicates import And as _And
from .predicates import Backlog as _Backlog
from .predicates import From as _From
from .predicates import Not as _Not
from .predicates import Or as _Or
from .predicates import Query
from .predicates import When
from .tracer import Tracer

try:
from ._version import version as __version__
Expand All @@ -59,6 +59,7 @@
'VarsPrinter',
'VarsSnooper',
'When',
'Backlog',

'stop',
'trace',
Expand Down Expand Up @@ -271,6 +272,32 @@ def From(condition=None, predicate=None, watermark=0, **kwargs):
return _From(condition, predicate, watermark)


def Backlog(condition=None, action=CallPrinter, size=100, stack_depth=0, **kwargs):
condition = _get_condition(condition, kwargs)
if inspect.isclass(action):
action = action()

return _Backlog(condition, action, size, stack_depth)


def _backlog_filter(self, condition=None, **kwargs):
self.filter_condition = _get_condition(condition, kwargs)
return self


_Backlog.filter = _backlog_filter
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use tricks like this as it would prevent a certain class of optimizations in the future Cython implementation. Everything needs to be implemented in the predicate class (a filter method that just copies itself + the filter args - it only needs to import Q locally).



def _get_condition(condition, kwargs):
if condition is None:
return Q(**kwargs)
elif kwargs:
raise TypeError("Unexpected arguments {}. Don't combine positional with keyword arguments.".format(
kwargs.keys()))

return condition


def stop():
"""
Stop tracing. Restores previous tracer (if there was any).
Expand Down
13 changes: 3 additions & 10 deletions src/hunter/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .util import PY3
from .util import StringType
from .util import builtins
from .util import frame_iterator
from .util import iter_symbols
from .util import safe_repr

Expand Down Expand Up @@ -418,10 +419,10 @@ class CallPrinter(CodePrinter):
.. versionadded:: 1.2.0
"""
EVENT_COLORS = CALL_COLORS
locals = defaultdict(list)

def __init__(self, *args, **kwargs):
super(CallPrinter, self).__init__(*args, **kwargs)
self.locals = defaultdict(list)

def __call__(self, event):
"""
Expand Down Expand Up @@ -791,7 +792,7 @@ def __call__(self, event):
frame.f_lineno,
frame.f_code.co_name
)
for frame in islice(self.frame_iterator(event.frame.f_back), self.depth)
for frame in islice(frame_iterator(event.frame.f_back), self.depth)
)
)
else:
Expand All @@ -806,11 +807,3 @@ def __call__(self, event):
thread_prefix,
filename_prefix,
)

def frame_iterator(self, frame):
"""
Yields frames till there are no more.
"""
while frame:
yield frame
frame = frame.f_back
17 changes: 13 additions & 4 deletions src/hunter/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .const import SYS_PREFIX_PATHS
from .util import CYTHON_SUFFIX_RE
from .util import LEADING_WHITESPACE_RE
from .util import MISSING
from .util import cached_property
from .util import get_func_in_mro
from .util import get_main_thread
Expand Down Expand Up @@ -40,7 +41,15 @@ class Event(object):
depth = None
calls = None

def __init__(self, frame, kind, arg, tracer):
def __init__(self, frame, kind, arg, tracer=None, depth=MISSING, calls=MISSING, threading_support=MISSING):
if tracer is None:
if MISSING in (depth, calls, threading_support):
raise TypeError("Depth, calls and threading support need to be specified when creating and event")
else:
depth = tracer.depth
calls = tracer.calls
threading_support = tracer.threading_support

#: The original Frame object.
#:
#: .. note::
Expand All @@ -61,12 +70,12 @@ def __init__(self, frame, kind, arg, tracer):
#: Tracing depth (increases on calls, decreases on returns)
#:
#: :type: int
self.depth = tracer.depth
self.depth = depth

#: A counter for total number of calls up to this Event.
#:
#: :type: int
self.calls = tracer.calls
self.calls = calls

#: A copy of the :attr:`hunter.tracer.Tracer.threading_support` flag.
#:
Expand All @@ -75,7 +84,7 @@ def __init__(self, frame, kind, arg, tracer):
#: Not allowed in the builtin predicates. You may access it from your custom predicate though.
#:
#: :type: bool or None
self.threading_support = tracer.threading_support
self.threading_support = threading_support

#: Flag that is ``True`` if the event was created with :meth:`~hunter.event.Event.detach`.
#:
Expand Down
131 changes: 127 additions & 4 deletions src/hunter/predicates.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from __future__ import absolute_import

import collections
import inspect
import re
from itertools import chain
from itertools import chain, islice

from .actions import Action
from .event import Event
from .util import StringType
from .util import StringType, clone_event_and_set_attrs
from .util import frame_iterator

__all__ = (
'And',
Expand All @@ -28,6 +30,10 @@
)


class BasePredicate(object):
pass


class Query(object):
"""
A query class.
Expand Down Expand Up @@ -82,13 +88,15 @@ def __init__(self, **query):
if operator in ('startswith', 'sw'):
if not isinstance(value, StringType):
if not isinstance(value, (list, set, tuple)):
raise ValueError('Value %r for %r is invalid. Must be a string, list, tuple or set.' % (value, key))
raise ValueError(
'Value %r for %r is invalid. Must be a string, list, tuple or set.' % (value, key))
value = tuple(value)
mapping = query_startswith
elif operator in ('endswith', 'ew'):
if not isinstance(value, StringType):
if not isinstance(value, (list, set, tuple)):
raise ValueError('Value %r for %r is invalid. Must be a string, list, tuple or set.' % (value, key))
raise ValueError(
'Value %r for %r is invalid. Must be a string, list, tuple or set.' % (value, key))
value = tuple(value)
mapping = query_endswith
elif operator == 'in':
Expand Down Expand Up @@ -643,3 +651,118 @@ def __rand__(self, other):
Convenience API so you can do ``other & Not(...)``. It converts that to ``And(other, Not(...))``.
"""
return And(other, self)


class Backlog(object):
def __init__(self, condition, action=None, size=100, stack_depth=0):
self.condition = condition
self.action = action
self.size = size
self.queue = collections.deque(maxlen=size)
self.stack_depth = stack_depth
self.called = False

self.filter_condition = lambda _: True

def __call__(self, event):
"""
Handles the event.
"""
if not self.called and self.condition(event):
self.called = True
for be in self._backlog_events_iter(event):
self.action(be)

return True

if not self.called and self.size and self.filter_condition(event):
event.detach()
self.queue.append(event)

return False

def _backlog_events_iter(self, event):
stack_events_length = self._compute_stack_events_length(event)
first_event = self.queue[0] if self.queue else event
first_frame = first_event.frame
initial_event_depth = first_event.depth

if first_event.kind == 'call':
first_frame = first_frame.f_back
initial_event_depth -= 1

stack_events = [
Event(
frame=frame, kind='call', arg=None,
threading_support=event.threading_support,
depth=initial_event_depth - i, calls=-1
)
for i, frame in enumerate(islice(frame_iterator(first_frame), stack_events_length))
]
stack_events.reverse()
yield from iter(stack_events)
yield from iter(self.queue)

def _compute_stack_events_length(self, event):
if len(self.queue) > 1:
backlog_depth_length = event.depth - self.queue[0].depth
if backlog_depth_length > 0:
stack_events_length = self.stack_depth - backlog_depth_length
if stack_events_length > 0:
return stack_events_length
else:
return 0

return self.stack_depth

def __str__(self):
return 'Backlog(%s, %s, size=%s, depth=%s)' % (
self.condition, self.action, self.size, self.stack_depth
)

def __repr__(self):
return '<hunter.predicates.Backlog: condition=%r, action=%r, size=%r, depth=%r>' % (
self.condition, self.action, self.size, self.stack_depth
)

def __eq__(self, other):
return (
isinstance(other, Backlog) and
self.condition == other.condition and
self.action == other.action and
self.queue == other.queue and
self.stack_depth == other.stack_depth
)

def __hash__(self):
return hash(('Backlog', self.condition, self.action, self.queue, self.stack_depth))

def __or__(self, other):
"""
Convenience API so you can do ``Backlog(...) | other``. It converts that to ``Or(Backlog(...), other)``.
"""
return Or(self, other)

def __and__(self, other):
"""
Convenience API so you can do ``Backlog(...) & other``. It converts that to ``And(Backlog(...), other))``.
"""
return And(self, other)

def __invert__(self):
"""
Convenience API so you can do ``~Backlog(...)``. It converts that to ``Not(Backlog(...))``.
"""
return Not(self)

def __ror__(self, other):
"""
Convenience API so you can do ``other | Backlog(...)``. It converts that to ``Or(other, Backlog(...))``.
"""
return Or(other, self)

def __rand__(self, other):
"""
Convenience API so you can do ``other & Backlog(...)``. It converts that to ``And(other, Backlog(...))``.
"""
return And(other, self)
16 changes: 16 additions & 0 deletions src/hunter/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,19 @@ def safe_repr(obj, maxdepth=5):
# if the object has a __dict__ then it's probably an instance of a pure python class, assume bad things
# with side-effects will be going on in __repr__ - use the default instead (object.__repr__)
return object.__repr__(obj)


def frame_iterator(frame):
"""
Yields frames till there are no more.
"""
while frame:
yield frame
frame = frame.f_back


def clone_event_and_set_attrs(event, **kwargs):
new_event = event.clone()
for key, val in kwargs.items():
setattr(new_event, key, val)
return new_event
7 changes: 7 additions & 0 deletions tests/sample7.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

import os
import sys
import hunter
# hunter.trace(hunter.Backlog(source_has='four', kind='call', size=2, stack_depth=10))#.filter(source_has='five'))
# hunter.trace(hunter.Backlog(fullsource_has='return i', size=5))
# hunter.trace(
#hunter.Backlog(fullsource_has='return i', size=5).filter(~hunter.Q(fullsource_has="five")),
# hunter.Q(fullsource_has='return i') | hunter.Q(fullsource_contains='three') | hunter.Q(fullsource_contains='four'), kind_in=['call', 'line']
# )


def one():
Expand Down
Loading