Skip to content

Commit 2d10ec1

Browse files
committed
Version 2.1.2
* Add in duplex module for dual instancemethod/classmethod definition * Update contextlog to allow for 'anonymous' logging (i.e. calling Log.output without having a reference to current log item)
1 parent 0ff3dc9 commit 2d10ec1

File tree

6 files changed

+181
-3
lines changed

6 files changed

+181
-3
lines changed

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
if __name__ == "__main__":
77
setup(
88
name="fin",
9-
version="2.1.1",
9+
version="2.1.2",
1010
license="BSD",
1111

1212
description="A small, useful python utility library",

src/fin/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
A utility library
33
"""
44

5-
VERSION = "2.1.1"
5+
VERSION = "2.1.2"

src/fin/contextlog.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
# -*- coding: utf-8 -*-
33

44
import collections
5+
import functools
56
import pprint
67
import sys
7-
import functools
88
import textwrap
9+
import types
910

1011
import fin.terminal
1112
import fin.color
13+
import fin.duplex
14+
1215

1316
THEMES = {
1417
"default": {
@@ -51,6 +54,13 @@ def __init__(self, msg):
5154
self.exit_msg = msg
5255

5356

57+
def find_open_log(cls):
58+
for stack in cls.LOGS.viewvalues():
59+
if len(stack) > 0:
60+
return stack[-1]
61+
raise ValueError("Cannot find a suitable context log to output to")
62+
63+
5464
class Log(object):
5565

5666
"""A logging context manager"""
@@ -128,7 +138,15 @@ def __exit__(self, exc_type, exc_value, tb):
128138
self.on_exit(exc_type is not None, msg)
129139
return rv
130140

141+
@fin.duplex.method(inst_lookup_fun=find_open_log)
131142
def output(self, msg):
143+
if isinstance(self, types.TypeType) and issubclass(self, Log):
144+
for stack in self.LOGS.viewvalues():
145+
if len(stack) > 0:
146+
self = stack[-1]
147+
break
148+
else:
149+
raise ValueError("Cannot find a suitable context log to output to")
132150
if not self.open:
133151
raise ValueError("Cannot log output from outside log context.")
134152
self.child_added(None)
@@ -143,6 +161,7 @@ def output(self, msg):
143161
self.stream.write(full)
144162
self.stream.flush()
145163

164+
@fin.duplex.method(inst_lookup_fun=find_open_log)
146165
def format(self, msg, **kwargs):
147166
if not self.open:
148167
raise ValueError("Cannot log output from outside log context.")

src/fin/contextlog_test.py

+20
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,26 @@ def test_log_output_format(self):
106106
l.format({1: "2"})
107107
self.assertEqual(self.lines, ["Foo: ", "| + {1: '2'}", "`- OK"])
108108

109+
def test_anonymous_output(self):
110+
with fin.contextlog.Log("Foo", stream=self):
111+
fin.contextlog.Log.output("Test")
112+
self.assertEqual(self.lines, ['Foo: ', '| + Test', '`- OK'])
113+
114+
def test_anonymous_nested_output(self):
115+
with fin.contextlog.Log("Foo", stream=self):
116+
with fin.contextlog.Log("Bar", stream=self):
117+
fin.contextlog.Log.output("Test")
118+
self.assertEqual(self.lines, ['Foo: ', '| Bar: ', '| | + Test', '| `- OK', '`- OK'])
119+
120+
def test_anonymous_output_noleak(self):
121+
with fin.contextlog.Log("Foo", stream=self):
122+
with fin.contextlog.Log("Bar", stream=self):
123+
fin.contextlog.Log.output("ONE")
124+
fin.contextlog.Log.output("TWO")
125+
self.assertEqual(self.lines, ['Foo: ', '| Bar: ', '| | + ONE', '| `- OK', '| + TWO', '`- OK'])
126+
127+
128+
109129

110130
if __name__ == "__main__":
111131
unittest.main()

src/fin/duplex.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import functools
2+
3+
4+
_property = property
5+
6+
7+
class DuplexMethod(object):
8+
9+
def __init__(self, func, class_method=None, inst_lookup_fun=None):
10+
self.func = func
11+
self.class_method = class_method
12+
self.inst_lookup_fun = inst_lookup_fun
13+
14+
def __get__(self, obj, cls=None):
15+
if obj is None:
16+
if self.class_method is not None:
17+
return self.class_method.__get__(cls, cls)
18+
elif self.inst_lookup_fun is not None:
19+
obj = self.inst_lookup_fun(cls)
20+
else:
21+
obj = cls
22+
return self.func.__get__(obj, cls)
23+
24+
def classmethod(self, func):
25+
self.class_method = func
26+
return self
27+
28+
29+
def method(*args, **kwargs):
30+
"""
31+
Create a duplex method. Duplex methods can be called as an instance OR a classmethod using the same
32+
function name. Usage is similar to @classmethod, but the first (self) argument to the function, when called,
33+
will be bound to either an instance OR a class object depending on how it is called.
34+
35+
Example:
36+
37+
class A(object):
38+
@fin.dumplex.method
39+
def me(obj):
40+
return obj
41+
42+
>>> A.me() == <class 'A'>
43+
>>> A().me() == <A object at 0x1006cf350>
44+
45+
"""
46+
47+
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
48+
func, = args
49+
return DuplexMethod(func)
50+
def wrapper(func):
51+
return DuplexMethod(func, *args, **kwargs)
52+
return wrapper

src/fin/duplex_test.py

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
2+
from __future__ import with_statement
3+
4+
import os
5+
import shutil
6+
import tempfile
7+
8+
import fin.testing
9+
import fin.duplex
10+
11+
12+
class DuplexTest(fin.testing.TestCase):
13+
14+
def test_duplex_definition(self):
15+
class A(object):
16+
def abc(self):
17+
return self
18+
19+
@fin.duplex.method
20+
def xyz(self):
21+
return self
22+
23+
a = A()
24+
self.assertIs(A.xyz(), A)
25+
self.assertIs(a.xyz(), a)
26+
27+
def test_duplex_definition_call(self):
28+
class A(object):
29+
def abc(self):
30+
return self
31+
32+
@fin.duplex.method()
33+
def xyz(self):
34+
return self
35+
36+
a = A()
37+
self.assertIs(A.xyz(), A)
38+
self.assertIs(a.xyz(), a)
39+
40+
def test_custom_lookup_func(self):
41+
def find_a(cls):
42+
return cls.INST
43+
44+
class A(object):
45+
INST = None
46+
@fin.duplex.method(inst_lookup_fun=find_a)
47+
def me(self):
48+
return self
49+
50+
a = A()
51+
A.INST = a
52+
self.assertIs(A.me(), a)
53+
self.assertIs(a.me(), a)
54+
55+
class B(A):
56+
pass
57+
b = B()
58+
self.assertIs(B.me(), a)
59+
self.assertIs(b.me(), b)
60+
61+
B.INST = b
62+
self.assertIs(A.me(), a)
63+
self.assertIs(a.me(), a)
64+
self.assertIs(B.me(), b)
65+
self.assertIs(b.me(), b)
66+
A.INST = None
67+
B.INST = None
68+
69+
def test_different_classmethod(self):
70+
INST = object()
71+
CLS = object()
72+
class A(object):
73+
@fin.duplex.method
74+
def thefunc(self):
75+
return INST, self
76+
77+
@thefunc.classmethod
78+
def thefunc(cls):
79+
return CLS, cls
80+
81+
a = A()
82+
self.assertEqual(A.thefunc(), (CLS, A))
83+
self.assertEqual(a.thefunc(), (INST, a))
84+
85+
86+
if __name__ == "__main__":
87+
fin.testing.main()

0 commit comments

Comments
 (0)