From 69bacee1d8f181bc00f632372ee3b7bc4eb48388 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Fri, 28 Oct 2016 11:41:46 +1300 Subject: Sketch out addons.View The first iteration of a replacement for addons.State --- mitmproxy/addons/view.py | 124 +++++++++++++++++++++++++++++++++++++ setup.py | 1 + test/mitmproxy/addons/test_view.py | 92 +++++++++++++++++++++++++++ 3 files changed, 217 insertions(+) create mode 100644 mitmproxy/addons/view.py create mode 100644 test/mitmproxy/addons/test_view.py diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py new file mode 100644 index 00000000..f79dd64d --- /dev/null +++ b/mitmproxy/addons/view.py @@ -0,0 +1,124 @@ +""" +The View: + +- Keeps track of a store of flows +- Maintains a filtered, ordered view onto that list of flows +- Exposes various operations on flows in the store - notably intercept and + resume +- Exposes a number of signals so the view can be monitored +""" +import collections +import typing +import datetime + +import blinker +import sortedcontainers + +from mitmproxy import flow +from mitmproxy import flowfilter + + +def key_request_start(f: flow.Flow) -> datetime.datetime: + return f.request.timestamp_start or 0 + + +def key_request_method(f: flow.Flow) -> str: + return f.request.method + + +matchall = flowfilter.parse(".") + + +class View(collections.Sequence): + def __init__(self): + super().__init__() + self._store = {} + self.filter = matchall + self.order_key = key_request_start + self.order_reverse = False + self._view = sortedcontainers.SortedListWithKey( + key = self.order_key + ) + + # These signals broadcast events that affect the view. That is, an + # update to a flow in the store but not in the view does not trigger a + # signal. + self.sig_update = blinker.Signal() + self.sig_add = blinker.Signal() + self.sig_remove = blinker.Signal() + # Signals that the view should be refreshed completely + self.sig_refresh = blinker.Signal() + + def __len__(self): + return len(self._view) + + def __getitem__(self, offset) -> flow.Flow: + if self.order_reverse: + offset = -offset - 1 + return self._view[offset] + + def set_order(self, order_key: typing.Callable): + """ + Sets the current view order. + """ + self.order_key = order_key + newview = sortedcontainers.SortedListWithKey(key=order_key) + newview.update(self._view) + self._view = newview + + def set_filter(self, flt: typing.Optional[flowfilter.TFilter]): + """ + Sets the current view filter. + """ + self.filter = flt or matchall + self._view.clear() + for i in self._store.values(): + if self.filter(i): + self._view.add(i) + + def clear(self): + """ + Clears both the state and view. + """ + self._state.clear() + self._view.clear() + + def add(self, f: flow.Flow): + """ + Adds a flow to the state. If the flow already exists, it is + ignored. + """ + if f.id not in self._store: + self._store[f.id] = f + if self.filter(f): + self._view.add(f) + + def update(self, f: flow.Flow): + """ + Updates a flow. If the flow is not in the state, it's ignored. + """ + if f.id in self._store: + if self.filter(f): + if f not in self._view: + self._view.add(f) + else: + try: + self._view.remove(f) + except ValueError: + pass + + # Event handlers + def request(self, f): + self.add(f) + + def intercept(self, f): + self.update(f) + + def resume(self, f): + self.update(f) + + def error(self, f): + self.update(f) + + def response(self, f): + self.update(f) diff --git a/setup.py b/setup.py index 70ff8b5d..fd291973 100644 --- a/setup.py +++ b/setup.py @@ -83,6 +83,7 @@ setup( "urwid>=1.3.1, <1.4", "watchdog>=0.8.3, <0.9", "brotlipy>=0.5.1, <0.7", + "sortedcontainers>=1.5.4, <1.6", ], extras_require={ ':sys_platform == "win32"': [ diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py new file mode 100644 index 00000000..3e01d71f --- /dev/null +++ b/test/mitmproxy/addons/test_view.py @@ -0,0 +1,92 @@ +from mitmproxy.addons import view +from mitmproxy import flowfilter +from .. import tutils + + +def test_simple(): + v = view.View() + f = tutils.tflow() + f.request.timestamp_start = 1 + v.request(f) + assert list(v) == [f] + v.request(f) + assert list(v) == [f] + assert len(v._store) == 1 + + f2 = tutils.tflow() + f2.request.timestamp_start = 3 + v.request(f2) + assert list(v) == [f, f2] + v.request(f2) + assert list(v) == [f, f2] + assert len(v._store) == 2 + + f3 = tutils.tflow() + f3.request.timestamp_start = 2 + v.request(f3) + assert list(v) == [f, f3, f2] + v.request(f3) + assert list(v) == [f, f3, f2] + assert len(v._store) == 3 + + +def tft(*, method="get", start=0): + f = tutils.tflow() + f.request.method = method + f.request.timestamp_start = start + return f + + +def test_filter(): + v = view.View() + f = flowfilter.parse("~m get") + v.request(tft(method="get")) + v.request(tft(method="put")) + v.request(tft(method="get")) + v.request(tft(method="put")) + assert(len(v)) == 4 + v.set_filter(f) + assert [i.request.method for i in v] == ["GET", "GET"] + assert len(v._store) == 4 + v.set_filter(None) + + +def test_order(): + v = view.View() + v.request(tft(method="get", start=1)) + v.request(tft(method="put", start=2)) + v.request(tft(method="get", start=3)) + v.request(tft(method="put", start=4)) + assert [i.request.timestamp_start for i in v] == [1, 2, 3, 4] + + v.set_order(view.key_request_method) + assert [i.request.method for i in v] == ["GET", "GET", "PUT", "PUT"] + v.order_reverse = True + assert [i.request.method for i in v] == ["PUT", "PUT", "GET", "GET"] + + v.set_order(view.key_request_start) + assert [i.request.timestamp_start for i in v] == [4, 3, 2, 1] + + v.order_reverse = False + assert [i.request.timestamp_start for i in v] == [1, 2, 3, 4] + + +def test_update(): + v = view.View() + flt = flowfilter.parse("~m get") + v.set_filter(flt) + + f = tft(method="get") + v.request(f) + assert f in v + + f.request.method = "put" + v.update(f) + assert f not in v + + f.request.method = "get" + v.update(f) + assert f in v + + v.update(f) + assert f in v -- cgit v1.2.3 From 9dcc3a3e2026abecc98d0eb3c4af80ea18ca0a69 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Fri, 28 Oct 2016 12:32:23 +1300 Subject: addons.View: hook up signals --- mitmproxy/addons/view.py | 14 +++++--- test/mitmproxy/addons/test_view.py | 71 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index f79dd64d..76f82ed6 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -36,13 +36,11 @@ class View(collections.Sequence): self.filter = matchall self.order_key = key_request_start self.order_reverse = False - self._view = sortedcontainers.SortedListWithKey( - key = self.order_key - ) + self._view = sortedcontainers.SortedListWithKey(key = self.order_key) # These signals broadcast events that affect the view. That is, an # update to a flow in the store but not in the view does not trigger a - # signal. + # signal. All signals are called after the view has been updated. self.sig_update = blinker.Signal() self.sig_add = blinker.Signal() self.sig_remove = blinker.Signal() @@ -75,6 +73,7 @@ class View(collections.Sequence): for i in self._store.values(): if self.filter(i): self._view.add(i) + self.sig_refresh.send(self) def clear(self): """ @@ -82,6 +81,7 @@ class View(collections.Sequence): """ self._state.clear() self._view.clear() + self.sig_refresh.send(self) def add(self, f: flow.Flow): """ @@ -92,6 +92,7 @@ class View(collections.Sequence): self._store[f.id] = f if self.filter(f): self._view.add(f) + self.sig_add.send(self, flow=f) def update(self, f: flow.Flow): """ @@ -101,10 +102,15 @@ class View(collections.Sequence): if self.filter(f): if f not in self._view: self._view.add(f) + self.sig_add.send(self, flow=f) + else: + self.sig_update.send(self, flow=f) else: try: self._view.remove(f) + self.sig_remove.send(self, flow=f) except ValueError: + # The value was not in the view pass # Event handlers diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index 3e01d71f..cff99621 100644 --- a/test/mitmproxy/addons/test_view.py +++ b/test/mitmproxy/addons/test_view.py @@ -1,5 +1,6 @@ from mitmproxy.addons import view from mitmproxy import flowfilter + from .. import tutils @@ -90,3 +91,73 @@ def test_update(): v.update(f) assert f in v + + +class Record: + def __init__(self): + self.calls = [] + + def __bool__(self): + return bool(self.calls) + + def __repr__(self): + return repr(self.calls) + + def __call__(self, *args, **kwargs): + self.calls.append((args, kwargs)) + + +def test_signals(): + v = view.View() + rec_add = Record() + rec_update = Record() + rec_remove = Record() + rec_refresh = Record() + + def clearrec(): + rec_add.calls = [] + rec_update.calls = [] + rec_remove.calls = [] + rec_refresh.calls = [] + + v.sig_add.connect(rec_add) + v.sig_update.connect(rec_update) + v.sig_remove.connect(rec_remove) + v.sig_refresh.connect(rec_refresh) + + assert not any([rec_add, rec_update, rec_remove, rec_refresh]) + + # Simple add + v.add(tft()) + assert rec_add + assert not any([rec_update, rec_remove, rec_refresh]) + + # Filter change triggers refresh + clearrec() + v.set_filter(flowfilter.parse("~m put")) + assert rec_refresh + assert not any([rec_update, rec_add, rec_remove]) + + v.set_filter(flowfilter.parse("~m get")) + + # An update that results in a flow being added to the view + clearrec() + v[0].request.method = "PUT" + v.update(v[0]) + assert rec_remove + assert not any([rec_update, rec_refresh, rec_add]) + + # An update that does not affect the view just sends update + v.set_filter(flowfilter.parse("~m put")) + clearrec() + v.update(v[0]) + assert rec_update + assert not any([rec_remove, rec_refresh, rec_add]) + + # An update for a flow in state but not view does not do anything + f = v[0] + v.set_filter(flowfilter.parse("~m get")) + assert not len(v) + clearrec() + v.update(f) + assert not any([rec_add, rec_update, rec_remove, rec_refresh]) -- cgit v1.2.3 From 12a70d03ad721f6a137d118b65c11851ef7b96d7 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 29 Oct 2016 10:17:29 +1300 Subject: addons.view: Add a focus tracker --- mitmproxy/addons/view.py | 70 ++++++++++++++++++++++++++++++++++++-- test/mitmproxy/addons/test_view.py | 55 ++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index 76f82ed6..99481376 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -3,9 +3,8 @@ The View: - Keeps track of a store of flows - Maintains a filtered, ordered view onto that list of flows -- Exposes various operations on flows in the store - notably intercept and - resume - Exposes a number of signals so the view can be monitored +- Has an associated class that tracks focus within the view """ import collections import typing @@ -94,6 +93,16 @@ class View(collections.Sequence): self._view.add(f) self.sig_add.send(self, flow=f) + def remove(self, f: flow.Flow): + """ + Removes the flow from the underlying store and the view. + """ + if f.id in self._store: + del self._store[f.id] + if f in self._view: + self._view.remove(f) + self.sig_remove.send(self, flow=f) + def update(self, f: flow.Flow): """ Updates a flow. If the flow is not in the state, it's ignored. @@ -113,6 +122,13 @@ class View(collections.Sequence): # The value was not in the view pass + # Reflect some methods to the efficient underlying implementation + def bisect(self, f: flow.Flow) -> int: + return self._view.bisect(f) + + def index(self, f: flow.Flow) -> int: + return self._view.index(f) + # Event handlers def request(self, f): self.add(f) @@ -128,3 +144,53 @@ class View(collections.Sequence): def response(self, f): self.update(f) + + +class Focus: + """ + Tracks a focus element within a View. + """ + def __init__(self, v: View) -> None: + self.view = v + self._focusflow = None + + self.focusflow = None + if len(self.view): + self.focusflow = self.view[0] + v.sig_add.connect(self._sig_add) + v.sig_remove.connect(self._sig_remove) + v.sig_refresh.connect(self._sig_refresh) + + @property + def focusflow(self) -> typing.Optional[flow.Flow]: + return self._focusflow + + @focusflow.setter + def focusflow(self, f: flow.Flow): + if f is not None and f not in self.view: + raise ValueError("Attempt to set focus to flow not in view") + self._focusflow = f + + @property + def index(self) -> typing.Optional[int]: + if self.focusflow: + return self.view.index(self.focusflow) + + def _sig_remove(self, view, flow): + if len(view) == 0: + self.focusflow = None + elif flow is self.focusflow: + idx = min(view.bisect(self.focusflow), len(view)-1) + self.focusflow = view[idx] + + def _sig_refresh(self, view): + if len(view) == 0: + self.focusflow = None + else: + if self.focusflow not in view: + self.focusflow = view[0] + + def _sig_add(self, view, flow): + # We only have to act if we don't have a focus element + if not self.focusflow: + self.focusflow = flow diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index cff99621..527464ad 100644 --- a/test/mitmproxy/addons/test_view.py +++ b/test/mitmproxy/addons/test_view.py @@ -161,3 +161,58 @@ def test_signals(): clearrec() v.update(f) assert not any([rec_add, rec_update, rec_remove, rec_refresh]) + + +def test_focus(): + # Special case - initialising with a view that already contains data + v = view.View() + v.add(tft()) + f = view.Focus(v) + assert f.index is 0 + assert f.focusflow is v[0] + + # Start empty + v = view.View() + f = view.Focus(v) + assert f.index is None + assert f.focusflow is None + + v.add(tft(start=1)) + assert f.index == 0 + assert f.focusflow is v[0] + + v.add(tft(start=0)) + assert f.index == 1 + assert f.focusflow is v[1] + + v.add(tft(start=2)) + assert f.index == 1 + assert f.focusflow is v[1] + + v.remove(v[1]) + assert f.index == 1 + assert f.focusflow is v[1] + + v.remove(v[1]) + assert f.index == 0 + assert f.focusflow is v[0] + + v.remove(v[0]) + assert f.index is None + assert f.focusflow is None + + v.add(tft(method="get", start=0)) + v.add(tft(method="get", start=1)) + v.add(tft(method="put", start=2)) + v.add(tft(method="get", start=3)) + + f.focusflow = v[2] + assert f.focusflow.request.method == "PUT" + + filt = flowfilter.parse("~m get") + v.set_filter(filt) + assert f.index == 0 + + filt = flowfilter.parse("~m oink") + v.set_filter(filt) + assert f.index is None -- cgit v1.2.3 From 90e7142b5c8dd3ec9896e5308ea2e81a2d77d31d Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 29 Oct 2016 10:55:57 +1300 Subject: addons.View: better order reversal Deal with some subtleties in order reversal, add a toggle method that emits refresh. --- mitmproxy/addons/view.py | 50 +++++++++++++++++++++++++++----------- test/mitmproxy/addons/test_view.py | 21 ++++++++++++++-- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index 99481376..b5682b7a 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -34,7 +34,7 @@ class View(collections.Sequence): self._store = {} self.filter = matchall self.order_key = key_request_start - self.order_reverse = False + self.order_reversed = False self._view = sortedcontainers.SortedListWithKey(key = self.order_key) # These signals broadcast events that affect the view. That is, an @@ -46,13 +46,42 @@ class View(collections.Sequence): # Signals that the view should be refreshed completely self.sig_refresh = blinker.Signal() + def _rev(self, idx: int) -> int: + """ + Reverses an index, if needed + """ + if self.order_reversed: + if idx < 0: + idx = -idx - 1 + else: + idx = len(self._view) - idx - 1 + if idx < 0: + raise IndexError + return idx + def __len__(self): return len(self._view) def __getitem__(self, offset) -> flow.Flow: - if self.order_reverse: - offset = -offset - 1 - return self._view[offset] + return self._view[self._rev(offset)] + + # Reflect some methods to the efficient underlying implementation + + def bisect(self, f: flow.Flow) -> int: + v = self._view.bisect(f) + # Bisect returns an item to the RIGHT of the existing entries. + if v == 0: + return v + return self._rev(v - 1) + 1 + + def index(self, f: flow.Flow) -> int: + return self._rev(self._view.index(f)) + + # API + + def toggle_reversed(self): + self.order_reversed = not self.order_reversed + self.sig_refresh.send(self) def set_order(self, order_key: typing.Callable): """ @@ -122,14 +151,8 @@ class View(collections.Sequence): # The value was not in the view pass - # Reflect some methods to the efficient underlying implementation - def bisect(self, f: flow.Flow) -> int: - return self._view.bisect(f) - - def index(self, f: flow.Flow) -> int: - return self._view.index(f) - # Event handlers + def request(self, f): self.add(f) @@ -186,9 +209,8 @@ class Focus: def _sig_refresh(self, view): if len(view) == 0: self.focusflow = None - else: - if self.focusflow not in view: - self.focusflow = view[0] + elif self.focusflow not in view: + self.focusflow = view[0] def _sig_add(self, view, flow): # We only have to act if we don't have a focus element diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index 527464ad..1ab7e8f4 100644 --- a/test/mitmproxy/addons/test_view.py +++ b/test/mitmproxy/addons/test_view.py @@ -62,16 +62,33 @@ def test_order(): v.set_order(view.key_request_method) assert [i.request.method for i in v] == ["GET", "GET", "PUT", "PUT"] - v.order_reverse = True + v.toggle_reversed() assert [i.request.method for i in v] == ["PUT", "PUT", "GET", "GET"] v.set_order(view.key_request_start) assert [i.request.timestamp_start for i in v] == [4, 3, 2, 1] - v.order_reverse = False + v.toggle_reversed() assert [i.request.timestamp_start for i in v] == [1, 2, 3, 4] +def test_reversed(): + v = view.View() + v.request(tft(start=1)) + v.request(tft(start=2)) + v.request(tft(start=3)) + v.toggle_reversed() + + assert v[0].request.timestamp_start == 3 + assert v[-1].request.timestamp_start == 1 + assert v[2].request.timestamp_start == 1 + tutils.raises(IndexError, v.__getitem__, 5) + tutils.raises(IndexError, v.__getitem__, -5) + + assert v.bisect(v[0]) == 1 + assert v.bisect(v[2]) == 3 + + def test_update(): v = view.View() flt = flowfilter.parse("~m get") -- cgit v1.2.3 From 14df96943470046b788c9e2dfec37610a378f6a3 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 29 Oct 2016 11:08:35 +1300 Subject: addons.view.focus: Better handling of view refresh When we refresh and our current focus goes out of scope, we set the focus to the element nearest the old focus. --- mitmproxy/addons/view.py | 8 +++++--- test/mitmproxy/addons/test_view.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index b5682b7a..d8d6e853 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -199,18 +199,20 @@ class Focus: if self.focusflow: return self.view.index(self.focusflow) + def _nearest(self, f, v): + return min(v.bisect(f), len(v)-1) + def _sig_remove(self, view, flow): if len(view) == 0: self.focusflow = None elif flow is self.focusflow: - idx = min(view.bisect(self.focusflow), len(view)-1) - self.focusflow = view[idx] + self.focusflow = view[self._nearest(self.focusflow, view)] def _sig_refresh(self, view): if len(view) == 0: self.focusflow = None elif self.focusflow not in view: - self.focusflow = view[0] + self.focusflow = view[self._nearest(self.focusflow, view)] def _sig_add(self, view, flow): # We only have to act if we don't have a focus element diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index 1ab7e8f4..56372749 100644 --- a/test/mitmproxy/addons/test_view.py +++ b/test/mitmproxy/addons/test_view.py @@ -228,7 +228,7 @@ def test_focus(): filt = flowfilter.parse("~m get") v.set_filter(filt) - assert f.index == 0 + assert f.index == 2 filt = flowfilter.parse("~m oink") v.set_filter(filt) -- cgit v1.2.3 From 32a0a7b8600c949a70ddabd332caca415ef82d42 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 29 Oct 2016 11:37:32 +1300 Subject: addons.view: flow settings Add a flow settings mechanism, enable focus and settings unilaterally. --- mitmproxy/addons/view.py | 37 ++++++++++++++++++++++++++++++++++++- test/mitmproxy/addons/test_view.py | 15 +++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index d8d6e853..e2224c58 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -4,7 +4,9 @@ The View: - Keeps track of a store of flows - Maintains a filtered, ordered view onto that list of flows - Exposes a number of signals so the view can be monitored -- Has an associated class that tracks focus within the view +- Tracks focus within the view +- Exposes a settings store for flows that automatically expires if the flow is + removed from the store. """ import collections import typing @@ -46,6 +48,9 @@ class View(collections.Sequence): # Signals that the view should be refreshed completely self.sig_refresh = blinker.Signal() + self.focus = Focus(self) + self.settings = Settings(self) + def _rev(self, idx: int) -> int: """ Reverses an index, if needed @@ -211,6 +216,8 @@ class Focus: def _sig_refresh(self, view): if len(view) == 0: self.focusflow = None + elif self.focusflow is None: + self.focusflow = view[0] elif self.focusflow not in view: self.focusflow = view[self._nearest(self.focusflow, view)] @@ -218,3 +225,31 @@ class Focus: # We only have to act if we don't have a focus element if not self.focusflow: self.focusflow = flow + + +class Settings(collections.Mapping): + def __init__(self, view: View) -> None: + self.view = view + self.values = {} + view.sig_remove.connect(self._sig_remove) + view.sig_refresh.connect(self._sig_refresh) + + def __iter__(self) -> typing.Iterable: + return iter(self.values) + + def __len__(self) -> int: + return len(self.values) + + def __getitem__(self, f: flow.Flow) -> dict: + if f.id not in self.view._store: + raise KeyError + return self.values.setdefault(f.id, {}) + + def _sig_remove(self, view, flow): + if flow.id in self.values: + del self.values[flow.id] + + def _sig_refresh(self, view): + for fid in self.values.keys(): + if fid not in view._store: + del self.values[fid] diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index 56372749..1404a78a 100644 --- a/test/mitmproxy/addons/test_view.py +++ b/test/mitmproxy/addons/test_view.py @@ -233,3 +233,18 @@ def test_focus(): filt = flowfilter.parse("~m oink") v.set_filter(filt) assert f.index is None + + +def test_settings(): + v = view.View() + f = tft() + + tutils.raises(KeyError, v.settings.__getitem__, f) + v.add(f) + assert v.settings[f] == {} + v.settings[f]["foo"] = "bar" + assert v.settings[f]["foo"] == "bar" + assert len(list(v.settings)) == 1 + v.remove(f) + tutils.raises(KeyError, v.settings.__getitem__, f) + assert not v.settings.keys() -- cgit v1.2.3 From 7ecaeb02145b1c9c514e65a81d3d1ae231dc681b Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 29 Oct 2016 11:50:10 +1300 Subject: addons.view.focus: next and prev methods --- mitmproxy/addons/view.py | 20 +++++++++++++++++--- test/mitmproxy/addons/test_view.py | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index e2224c58..af366d67 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -181,8 +181,6 @@ class Focus: def __init__(self, v: View) -> None: self.view = v self._focusflow = None - - self.focusflow = None if len(self.view): self.focusflow = self.view[0] v.sig_add.connect(self._sig_add) @@ -204,8 +202,24 @@ class Focus: if self.focusflow: return self.view.index(self.focusflow) + def next(self): + """ + Sets the focus to the next flow. + """ + if self.focusflow: + idx = min(self.index + 1, len(self.view) - 1) + self.focusflow = self.view[idx] + + def prev(self): + """ + Sets the focus to the previous flow. + """ + if self.focusflow: + idx = max(self.index - 1, 0) + self.focusflow = self.view[idx] + def _nearest(self, f, v): - return min(v.bisect(f), len(v)-1) + return min(v.bisect(f), len(v) - 1) def _sig_remove(self, view, flow): if len(view) == 0: diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index 1404a78a..e4083bac 100644 --- a/test/mitmproxy/addons/test_view.py +++ b/test/mitmproxy/addons/test_view.py @@ -235,6 +235,31 @@ def test_focus(): assert f.index is None +def test_focus_nextprev(): + v = view.View() + # Nops on an empty view + v.focus.next() + v.focus.prev() + + # Nops on a single-flow view + v.add(tft(start=0)) + assert v.focus.focusflow == v[0] + v.focus.next() + assert v.focus.focusflow == v[0] + v.focus.prev() + assert v.focus.focusflow == v[0] + + v.add(tft(start=1)) + v.focus.next() + assert v.focus.focusflow == v[1] + v.focus.next() + assert v.focus.focusflow == v[1] + v.focus.prev() + assert v.focus.focusflow == v[0] + v.focus.prev() + assert v.focus.focusflow == v[0] + + def test_settings(): v = view.View() f = tft() -- cgit v1.2.3 From 005c22445b013fdbce06569966cd86a48201e837 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sat, 29 Oct 2016 11:55:44 +1300 Subject: addons.view: focus.focusflow -> focus.flow --- mitmproxy/addons/view.py | 66 +++++++++++++++++++------------------- test/mitmproxy/addons/test_view.py | 34 ++++++++++---------- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index af366d67..8c0567a5 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -15,15 +15,15 @@ import datetime import blinker import sortedcontainers -from mitmproxy import flow +import mitmproxy.flow from mitmproxy import flowfilter -def key_request_start(f: flow.Flow) -> datetime.datetime: +def key_request_start(f: mitmproxy.flow.Flow) -> datetime.datetime: return f.request.timestamp_start or 0 -def key_request_method(f: flow.Flow) -> str: +def key_request_method(f: mitmproxy.flow.Flow) -> str: return f.request.method @@ -67,19 +67,19 @@ class View(collections.Sequence): def __len__(self): return len(self._view) - def __getitem__(self, offset) -> flow.Flow: + def __getitem__(self, offset) -> mitmproxy.flow.Flow: return self._view[self._rev(offset)] # Reflect some methods to the efficient underlying implementation - def bisect(self, f: flow.Flow) -> int: + def bisect(self, f: mitmproxy.flow.Flow) -> int: v = self._view.bisect(f) # Bisect returns an item to the RIGHT of the existing entries. if v == 0: return v return self._rev(v - 1) + 1 - def index(self, f: flow.Flow) -> int: + def index(self, f: mitmproxy.flow.Flow) -> int: return self._rev(self._view.index(f)) # API @@ -116,7 +116,7 @@ class View(collections.Sequence): self._view.clear() self.sig_refresh.send(self) - def add(self, f: flow.Flow): + def add(self, f: mitmproxy.flow.Flow): """ Adds a flow to the state. If the flow already exists, it is ignored. @@ -127,7 +127,7 @@ class View(collections.Sequence): self._view.add(f) self.sig_add.send(self, flow=f) - def remove(self, f: flow.Flow): + def remove(self, f: mitmproxy.flow.Flow): """ Removes the flow from the underlying store and the view. """ @@ -137,7 +137,7 @@ class View(collections.Sequence): self._view.remove(f) self.sig_remove.send(self, flow=f) - def update(self, f: flow.Flow): + def update(self, f: mitmproxy.flow.Flow): """ Updates a flow. If the flow is not in the state, it's ignored. """ @@ -180,65 +180,65 @@ class Focus: """ def __init__(self, v: View) -> None: self.view = v - self._focusflow = None + self._flow = None if len(self.view): - self.focusflow = self.view[0] + self.flow = self.view[0] v.sig_add.connect(self._sig_add) v.sig_remove.connect(self._sig_remove) v.sig_refresh.connect(self._sig_refresh) @property - def focusflow(self) -> typing.Optional[flow.Flow]: - return self._focusflow + def flow(self) -> typing.Optional[mitmproxy.flow.Flow]: + return self._flow - @focusflow.setter - def focusflow(self, f: flow.Flow): + @flow.setter + def flow(self, f: mitmproxy.flow.Flow): if f is not None and f not in self.view: raise ValueError("Attempt to set focus to flow not in view") - self._focusflow = f + self._flow = f @property def index(self) -> typing.Optional[int]: - if self.focusflow: - return self.view.index(self.focusflow) + if self.flow: + return self.view.index(self.flow) def next(self): """ Sets the focus to the next flow. """ - if self.focusflow: + if self.flow: idx = min(self.index + 1, len(self.view) - 1) - self.focusflow = self.view[idx] + self.flow = self.view[idx] def prev(self): """ Sets the focus to the previous flow. """ - if self.focusflow: + if self.flow: idx = max(self.index - 1, 0) - self.focusflow = self.view[idx] + self.flow = self.view[idx] def _nearest(self, f, v): return min(v.bisect(f), len(v) - 1) def _sig_remove(self, view, flow): if len(view) == 0: - self.focusflow = None - elif flow is self.focusflow: - self.focusflow = view[self._nearest(self.focusflow, view)] + self.flow = None + elif flow is self.flow: + self.flow = view[self._nearest(self.flow, view)] def _sig_refresh(self, view): if len(view) == 0: - self.focusflow = None - elif self.focusflow is None: - self.focusflow = view[0] - elif self.focusflow not in view: - self.focusflow = view[self._nearest(self.focusflow, view)] + self.flow = None + elif self.flow is None: + self.flow = view[0] + elif self.flow not in view: + self.flow = view[self._nearest(self.flow, view)] def _sig_add(self, view, flow): # We only have to act if we don't have a focus element - if not self.focusflow: - self.focusflow = flow + if not self.flow: + self.flow = flow class Settings(collections.Mapping): @@ -254,7 +254,7 @@ class Settings(collections.Mapping): def __len__(self) -> int: return len(self.values) - def __getitem__(self, f: flow.Flow) -> dict: + def __getitem__(self, f: mitmproxy.flow.Flow) -> dict: if f.id not in self.view._store: raise KeyError return self.values.setdefault(f.id, {}) diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index e4083bac..15cf534e 100644 --- a/test/mitmproxy/addons/test_view.py +++ b/test/mitmproxy/addons/test_view.py @@ -186,45 +186,45 @@ def test_focus(): v.add(tft()) f = view.Focus(v) assert f.index is 0 - assert f.focusflow is v[0] + assert f.flow is v[0] # Start empty v = view.View() f = view.Focus(v) assert f.index is None - assert f.focusflow is None + assert f.flow is None v.add(tft(start=1)) assert f.index == 0 - assert f.focusflow is v[0] + assert f.flow is v[0] v.add(tft(start=0)) assert f.index == 1 - assert f.focusflow is v[1] + assert f.flow is v[1] v.add(tft(start=2)) assert f.index == 1 - assert f.focusflow is v[1] + assert f.flow is v[1] v.remove(v[1]) assert f.index == 1 - assert f.focusflow is v[1] + assert f.flow is v[1] v.remove(v[1]) assert f.index == 0 - assert f.focusflow is v[0] + assert f.flow is v[0] v.remove(v[0]) assert f.index is None - assert f.focusflow is None + assert f.flow is None v.add(tft(method="get", start=0)) v.add(tft(method="get", start=1)) v.add(tft(method="put", start=2)) v.add(tft(method="get", start=3)) - f.focusflow = v[2] - assert f.focusflow.request.method == "PUT" + f.flow = v[2] + assert f.flow.request.method == "PUT" filt = flowfilter.parse("~m get") v.set_filter(filt) @@ -243,21 +243,21 @@ def test_focus_nextprev(): # Nops on a single-flow view v.add(tft(start=0)) - assert v.focus.focusflow == v[0] + assert v.focus.flow == v[0] v.focus.next() - assert v.focus.focusflow == v[0] + assert v.focus.flow == v[0] v.focus.prev() - assert v.focus.focusflow == v[0] + assert v.focus.flow == v[0] v.add(tft(start=1)) v.focus.next() - assert v.focus.focusflow == v[1] + assert v.focus.flow == v[1] v.focus.next() - assert v.focus.focusflow == v[1] + assert v.focus.flow == v[1] v.focus.prev() - assert v.focus.focusflow == v[0] + assert v.focus.flow == v[0] v.focus.prev() - assert v.focus.focusflow == v[0] + assert v.focus.flow == v[0] def test_settings(): -- cgit v1.2.3