Skip to content

Commit 741dff3

Browse files
committed
Merge branch 'origin/development'
2 parents fa6a5c7 + 7c60b86 commit 741dff3

File tree

24 files changed

+933
-97
lines changed

24 files changed

+933
-97
lines changed

README.markdown

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Please keep in mind that Python Toolbox is still in alpha stage, and that backwa
3434

3535
## Present ##
3636

37-
Python Toolbox is at version 0.6.0, which is an alpha release. It's being used in production every day, but backward compatibility isn't guaranteed yet.
37+
Python Toolbox is at version 0.6.2, which is an alpha release. It's being used in production every day, but backward compatibility isn't guaranteed yet.
3838

3939
## Next tasks ##
4040

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@
4545
# built documents.
4646
#
4747
# The short X.Y version.
48-
version = '0.6.0'
48+
version = '0.6.2'
4949
# The full version, including alpha/beta/rc tags.
50-
release = '0.6.0'
50+
release = '0.6.2'
5151

5252
# The language for content autogenerated by Sphinx. Refer to documentation
5353
# for a list of supported languages.

setup.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111

1212
### Confirming correct Python version: ########################################
1313
# #
14-
if sys.version_info[:2] <= (2, 5):
14+
if sys.version_info[:2] <= (2, 6):
1515
raise Exception(
16-
"You're using Python <= 2.5, but this package requires either Python "
16+
"You're using Python <= 2.6, but this package requires either Python "
1717
"2.7, or 3.3 or above, so you can't use it unless you upgrade your "
1818
"Python version."
1919
)
@@ -117,7 +117,7 @@ def get_packages():
117117
Present
118118
-------
119119
120-
Python Toolbox is at version 0.6.0, which is an alpha release. It's being used\
120+
Python Toolbox is at version 0.6.2, which is an alpha release. It's being used
121121
in production every day, but backward compatibility isn't guaranteed yet.
122122
123123
Next tasks
@@ -147,7 +147,7 @@ def get_packages():
147147

148148
setuptools.setup(
149149
name='python_toolbox',
150-
version='0.6.0',
150+
version='0.6.2',
151151
requires=['setuptools'],
152152
test_suite='nose.collector',
153153
install_requires=['setuptools'],

source_py2/python_toolbox/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@
1616
import python_toolbox.monkeypatch_copy_reg
1717
import python_toolbox.monkeypatch_envelopes
1818

19-
__version_info__ = python_toolbox.version_info.VersionInfo(0, 6, 0)
19+
__version_info__ = python_toolbox.version_info.VersionInfo(0, 6, 2)
2020
__version__ = __version_info__.version_text
2121

source_py2/python_toolbox/cute_iter_tools.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -320,9 +320,30 @@ def _fill(iterable, fill_value=None, fill_value_maker=None, length=infinity):
320320
yield fill_value_maker()
321321

322322

323+
def get_single_if_any(iterable,
324+
exception_on_multiple=Exception('More than one value '
325+
'not allowed.')):
326+
'''
327+
Get the single item of `iterable`, if any.
323328
324-
325-
326-
327-
328-
329+
If `iterable` has one item, return it. If it's empty, get `None`. If it has
330+
more than one item, raise an exception. (Unless
331+
`exception_on_multiple=None`.)
332+
'''
333+
assert isinstance(exception_on_multiple, Exception) or \
334+
exception_on_multiple is None
335+
iterator = iter(iterable)
336+
try:
337+
first_item = next(iterator)
338+
except StopIteration:
339+
return None
340+
else:
341+
if exception_on_multiple:
342+
try:
343+
second_item = next(iterator)
344+
except StopIteration:
345+
return first_item
346+
else:
347+
raise exception_on_multiple
348+
else: # not exception_on_multiple
349+
return first_item

source_py2/python_toolbox/monkeypatching_tools.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,29 @@ def decorator(function):
5454
else:
5555
# `function` is probably some kind of descriptor.
5656
if not monkeypatchee_is_a_class:
57-
raise NotImplemented("I don't know how to monkeypatch a "
58-
"descriptor onto a non-class object.")
59-
### Getting name of descriptor: ###################################
60-
# #
61-
if isinstance(function, caching.CachedProperty):
62-
if not isinstance(function.getter, types.FunctionType):
63-
raise NotImplemented
64-
name_ = function.getter.__name__
65-
elif isinstance(function, (classmethod, staticmethod)):
66-
name_ = function.__func__.__name__
67-
else:
68-
raise NotImplemented("`monkeypatch_method` doesn't know how "
69-
"to handle this kind of function.")
70-
# #
71-
### Finished getting name of descriptor. ##########################
57+
raise NotImplementedError(
58+
"I don't know how to monkeypatch a descriptor onto a "
59+
"non-class object."
60+
)
61+
if not name:
62+
### Getting name of descriptor: ###############################
63+
# #
64+
if isinstance(function, caching.CachedProperty):
65+
if not isinstance(function.getter, types.FunctionType):
66+
raise NotImplemented
67+
name_ = function.getter.__name__
68+
elif isinstance(function, (classmethod, staticmethod)):
69+
name_ = function.__func__.__name__
70+
elif isinstance(function, property):
71+
name_ = function.fget.__name__
72+
else:
73+
raise NotImplementedError(
74+
"`monkeypatch_method` doesn't know how to get the "
75+
"name of this kind of function automatically, try "
76+
"manually."
77+
)
78+
# #
79+
### Finished getting name of descriptor. ######################
7280
setattr(monkeypatchee, name_, function)
7381
return function
7482

source_py2/python_toolbox/nifty_collections/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from .weak_key_default_dict import WeakKeyDefaultDict
99
from .weak_key_identity_dict import WeakKeyIdentityDict
1010
from .lazy_tuple import LazyTuple
11+
from .frozen_dict import FrozenDict
12+
from .frozen_counter import FrozenCounter
1113

1214
from .emitting_ordered_set import EmittingOrderedSet
1315
from .emitting_weak_key_default_dict import EmittingWeakKeyDefaultDict
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Copyright 2009-2013 Ram Rachum.,
2+
# This program is distributed under the MIT license.
3+
4+
import operator
5+
import _heapq
6+
import itertools
7+
import collections
8+
9+
from .frozen_dict import FrozenDict
10+
11+
12+
class FrozenCounter(FrozenDict):
13+
'''
14+
An immutable counter.
15+
16+
A counter that can't be changed. The advantage of this over
17+
`collections.Counter` is mainly that it's hashable, and thus can be used as
18+
a key in dicts and sets.
19+
20+
In other words, `FrozenCounter` is to `Counter` what `frozenset` is to
21+
`set`.
22+
'''
23+
24+
def __init__(self, iterable=None, **kwargs):
25+
super(FrozenCounter, self).__init__()
26+
27+
if iterable is not None:
28+
if isinstance(iterable, collections.Mapping):
29+
self._dict.update(iterable)
30+
else:
31+
self_get = self._dict.get
32+
for element in iterable:
33+
self._dict[element] = self_get(element, 0) + 1
34+
if kwargs:
35+
self._dict.update(kwargs)
36+
37+
for key, value in self.items():
38+
if value == 0:
39+
del self._dict[key]
40+
41+
42+
__getitem__ = lambda self, key: self._dict.get(key, 0)
43+
44+
def most_common(self, n=None):
45+
'''
46+
List the `n` most common elements and their counts, sorted.
47+
48+
Results are sorted from the most common to the least. If `n is None`,
49+
then list all element counts.
50+
51+
>>> FrozenCounter('abcdeabcdabcaba').most_common(3)
52+
[('a', 5), ('b', 4), ('c', 3)]
53+
54+
'''
55+
# Emulate Bag.sortedByCount from Smalltalk
56+
if n is None:
57+
return sorted(self.iteritems(), key=operator.itemgetter(1),
58+
reverse=True)
59+
return _heapq.nlargest(n, self.iteritems(),
60+
key=operator.itemgetter(1))
61+
62+
def elements(self):
63+
'''
64+
Iterate over elements repeating each as many times as its count.
65+
66+
>>> c = FrozenCounter('ABCABC')
67+
>>> sorted(c.elements())
68+
['A', 'A', 'B', 'B', 'C', 'C']
69+
70+
# Knuth's example for prime factors of 1836: 2**2 * 3**3 * 17**1
71+
>>> prime_factors = FrozenCounter({2: 2, 3: 3, 17: 1})
72+
>>> product = 1
73+
>>> for factor in prime_factors.elements(): # loop over factors
74+
... product *= factor # and multiply them
75+
>>> product
76+
1836
77+
78+
Note, if an element's count has been set to zero or is a negative
79+
number, `.elements()` will ignore it.
80+
'''
81+
# Emulate Bag.do from Smalltalk and Multiset.begin from C++.
82+
return itertools.chain.from_iterable(
83+
itertools.starmap(itertools.repeat, self.iteritems())
84+
)
85+
86+
@classmethod
87+
def fromkeys(cls, iterable, v=None):
88+
# There is no equivalent method for counters because setting v=1
89+
# means that no element can have a count greater than one.
90+
raise NotImplementedError(
91+
'FrozenCounter.fromkeys() is undefined. Use '
92+
'FrozenCounter(iterable) instead.'
93+
)
94+
95+
def __repr__(self):
96+
if not self:
97+
return '%s()' % self.__class__.__name__
98+
try:
99+
items = ', '.join(map('%r: %r'.__mod__, self.most_common()))
100+
return '%s({%s})' % (self.__class__.__name__, items)
101+
except TypeError:
102+
# handle case where values are not orderable
103+
return '{0}({1!r})'.format(self.__class__.__name__, dict(self))
104+
105+
106+
__pos__ = lambda self: self
107+
__neg__ = lambda self: type(self)({key: -value for key, value
108+
in self.iteritems()})
109+
110+
# Multiset-style mathematical operations discussed in:
111+
# Knuth TAOCP Volume II section 4.6.3 exercise 19
112+
# and at http://en.wikipedia.org/wiki/Multiset
113+
#
114+
# Outputs guaranteed to only include positive counts.
115+
#
116+
# To strip negative and zero counts, add-in an empty counter:
117+
# c += FrozenCounter()
118+
119+
def __add__(self, other):
120+
'''
121+
Add counts from two counters.
122+
123+
>>> FrozenCounter('abbb') + FrozenCounter('bcc')
124+
FrozenCounter({'b': 4, 'c': 2, 'a': 1})
125+
126+
'''
127+
if not isinstance(other, FrozenCounter):
128+
return NotImplemented
129+
result = collections.Counter()
130+
for element, count in self.items():
131+
new_count = count + other[element]
132+
if new_count > 0:
133+
result[element] = new_count
134+
for element, count in other.items():
135+
if element not in self and count > 0:
136+
result[element] = count
137+
return FrozenCounter(result)
138+
139+
def __sub__(self, other):
140+
'''
141+
Subtract count, but keep only results with positive counts.
142+
143+
>>> FrozenCounter('abbbc') - FrozenCounter('bccd')
144+
FrozenCounter({'b': 2, 'a': 1})
145+
146+
'''
147+
if not isinstance(other, FrozenCounter):
148+
return NotImplemented
149+
result = collections.Counter()
150+
for element, count in self.items():
151+
new_count = count - other[element]
152+
if new_count > 0:
153+
result[element] = new_count
154+
for element, count in other.items():
155+
if element not in self and count < 0:
156+
result[element] = 0 - count
157+
return FrozenCounter(result)
158+
159+
def __or__(self, other):
160+
'''
161+
Get the maximum of value in either of the input counters.
162+
163+
>>> FrozenCounter('abbb') | FrozenCounter('bcc')
164+
FrozenCounter({'b': 3, 'c': 2, 'a': 1})
165+
166+
'''
167+
if not isinstance(other, FrozenCounter):
168+
return NotImplemented
169+
result = collections.Counter()
170+
for element, count in self.items():
171+
other_count = other[element]
172+
new_count = other_count if count < other_count else count
173+
if new_count > 0:
174+
result[element] = new_count
175+
for element, count in other.items():
176+
if element not in self and count > 0:
177+
result[element] = count
178+
return FrozenCounter(result)
179+
180+
def __and__(self, other):
181+
'''
182+
Get the minimum of corresponding counts.
183+
184+
>>> FrozenCounter('abbb') & FrozenCounter('bcc')
185+
FrozenCounter({'b': 1})
186+
187+
'''
188+
if not isinstance(other, FrozenCounter):
189+
return NotImplemented
190+
result = collections.Counter()
191+
for element, count in self.items():
192+
other_count = other[element]
193+
new_count = count if count < other_count else other_count
194+
if new_count > 0:
195+
result[element] = new_count
196+
return FrozenCounter(result)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2009-2013 Ram Rachum.
2+
# This program is distributed under the MIT license.
3+
4+
import collections
5+
import operator
6+
7+
8+
class FrozenDict(collections.Mapping):
9+
'''
10+
An immutable `dict`.
11+
12+
A `dict` that can't be changed. The advantage of this over `dict` is mainly
13+
that it's hashable, and thus can be used as a key in dicts and sets.
14+
15+
In other words, `FrozenDict` is to `dict` what `frozenset` is to `set`.
16+
'''
17+
18+
_hash = None # Overridden by instance when calculating hash.
19+
20+
def __init__(self, *args, **kwargs):
21+
self._dict = dict(*args, **kwargs)
22+
23+
__getitem__ = lambda self, key: self._dict[key]
24+
__len__ = lambda self: len(self._dict)
25+
__iter__ = lambda self: iter(self._dict)
26+
27+
def copy(self, *args, **kwargs):
28+
base_dict = self._dict.copy()
29+
base_dict.update(*args, **kwargs)
30+
return type(self)(base_dict)
31+
32+
def __hash__(self):
33+
if self._hash is None:
34+
self._hash = reduce(operator.xor,
35+
map(hash, self.iteritems()),
36+
0) ^ hash(len(self))
37+
38+
return self._hash
39+
40+
__repr__ = lambda self: '%s(%s)' % (type(self).__name__,
41+
repr(self._dict))
42+
__reduce__ = lambda self: (self.__class__, self._dict)
43+
44+

0 commit comments

Comments
 (0)