Last active
March 8, 2021 15:02
-
-
Save outofmbufs/f976925e21a2ea35bdfc6e33a8072db1 to your computer and use it in GitHub Desktop.
Python code to force something that *might* be a tuple (or a single thing) into a tuple. Useful(?) for processing arguments that can be a "thing" or multiple "things". Also defines a decorator (which seems like a horrible idea, but was fun!)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import functools | |
from collections import ChainMap | |
def tuplify(arg, *, keep=str): | |
"""Return tuple from an iterable or a naked single object. | |
If arg is a non-iterable object, returns: (arg,) | |
If arg is iterable, returns: tuple(arg) | |
Keyword argument 'keep' allows some iterables (most notably: strings) | |
to be treated as non-iterable, so that, for example: | |
tuplify('bozo', keep=str) | |
will return ('bozo',) not ('b', 'o', 'z', 'o') | |
This behavior for strings is actually the default, so: | |
tuplify('bozo') --> ('bozo',) | |
tuplify('bozo', keep=None) --> ('b', 'o', 'z', 'o') | |
Callers can use all the forms for 'keep' that isinstance supports; thus | |
'keep' itself can be a single type or a multi-level recursive tuple (!!) | |
""" | |
if keep is not None and isinstance(arg, keep): | |
return (arg,) | |
try: | |
return tuple(arg) | |
except TypeError: | |
return (arg,) | |
# this is fun, but probably a terrible idea ... a decorator to add tuplify() | |
# functionality to ONE specific argument of a function. The explicit design | |
# choice here is to only handle doing it for one particular argument; stack | |
# the decorator if multiple arguments must be tuplified. | |
# | |
def tuplifyarg(a): | |
"""decorator to tuplify a function argument. | |
a - kwarg name (e.g., 'foo') or positional arg index (e.g., 0) | |
CAVEAT CODER: the wrapper, invoked at call time, can only process the | |
arguments it gets; it cannot process omitted args that will take | |
on default values. Make sure to code any default arg values | |
accordingly (i.e., as tuples if that's what they need to be). | |
""" | |
# enforce some sanity at DECORATOR invocation time, rather than | |
# get very confusing errors later at function call time: | |
# argument 'a' must either be string-like or int-like | |
try: | |
bad = str(a) != a and int(a) != a | |
except TypeError: | |
bad = True | |
if bad: | |
raise TypeError(f"decorator argument 'a' (={a}) must be string or int") | |
# _deco is the outer/callable decorator object that will be called | |
# to decorate the function, and will (at that time) return the inner | |
# _wrapped() function which represents the wrapped/decorated function. | |
def _deco(f): | |
@functools.wraps(f) | |
def _wrapped(*args, **kwargs): | |
if str(a) == a: | |
if a in kwargs: | |
kwargs = ChainMap({a: tuplify(kwargs[a])}, kwargs) | |
else: | |
args = list(args) | |
args[a] = tuplify(args[a]) | |
return f(*args, **kwargs) | |
return _wrapped | |
return _deco | |
# if tuplifyarg is a terrible idea, so is this! tuplify ALL arguments | |
def tuplifyfunc(f): | |
@functools.wraps(f) | |
def _wrapped(*args, **kwargs): | |
tupled_args = [tuplify(a) for a in args] | |
tupled_kwargs = {k: tuplify(v) for k, v in kwargs.items()} | |
return f(*tupled_args, **tupled_kwargs) | |
return _wrapped | |
if __name__ == "__main__": | |
import unittest | |
class TestMethods(unittest.TestCase): | |
def test_basics(self): | |
a_tuple = (1, 2, 3) | |
a_string = "bozo" | |
same = object() # sentinel; means result same as v | |
as_a_tuple = object() # sentinel; means (v,) is result for v | |
for v, result, *keep in ( | |
(None, as_a_tuple), | |
(None, as_a_tuple, None), | |
(a_string, tuple(a_string), None), # string decomposed | |
(a_string, tuple(a_string), list), # string decomposed | |
(a_string, as_a_tuple), # string preserved... | |
(a_string, as_a_tuple, str), | |
(a_string, as_a_tuple, (str,)), | |
(a_string, as_a_tuple, (list, str)), | |
(a_string, as_a_tuple, (str, list)), | |
(a_string, as_a_tuple, (float, (int, (str,)))), | |
(tuple(), same), | |
(a_tuple, same), | |
(a_tuple, as_a_tuple, tuple), | |
(a_tuple, same, (str, int, float)), | |
(a_tuple, as_a_tuple, (str, int, float, tuple))): | |
if result is same: | |
result = v | |
elif result is as_a_tuple: | |
result = (v,) | |
with self.subTest(v=v, result=result, keep=keep): | |
t = tuplify(v, keep=keep[0]) if keep else tuplify(v) | |
self.assertEqual(t, result) | |
def test_deco_kwarg_1(self): | |
@tuplifyarg('bozo') | |
def clowns(*, krusty, bozo): | |
return len(bozo) | |
self.assertEqual(clowns(krusty=None, bozo=None), 1) | |
def test_deco_kwarg_2(self): | |
@tuplifyarg('krusty') | |
@tuplifyarg('bozo') | |
def clowns(*, krusty, bozo): | |
return len(krusty) + len(bozo) | |
self.assertEqual(clowns(krusty=None, bozo=(1, 2, 3)), 4) | |
def test_deco_arg_1(self): | |
@tuplifyarg(0) | |
def clowns(krusty, bozo): | |
return len(krusty) | |
self.assertEqual(clowns(None, None), 1) | |
def test_deco_arg_2(self): | |
with self.assertRaises(TypeError): | |
@tuplifyarg(1.2) | |
def foo(a, b): | |
pass | |
def test_deco_multiarg_1(self): | |
@tuplifyarg('b') | |
@tuplifyarg(0) | |
def lengths(a, b): | |
return len(a)+len(b) | |
self.assertEqual(lengths(None, b='abc'), 2) | |
def test_deco_multiarg_2(self): | |
@tuplifyarg(0) | |
@tuplifyarg('b') | |
def lengths(a, b='xyzzy'): | |
return len(a)+len(b) | |
self.assertEqual(lengths(None, b='abc'), 2) | |
# DEFAULT values of function arguments ARE NOT tuplified!! | |
self.assertEqual(lengths(None), 6) | |
def test_deco_func(self): | |
@tuplifyfunc | |
def foo(a, b, *, c='xyzzy', d=[]): | |
return len(a) + len(b) + len(c) + len(d) | |
self.assertEqual(foo(1, "bozo", d=[1, 2, 3]), 10) | |
# this test actually demonstrates/illustrates a limitation of | |
# the decorator/wrapper ... it cannot tuplify an omitted argument | |
# (because it doesn't even get to 'see' it). This could be used as | |
# a demented "is the argument defaulted" sentinel! (ick/haha) and | |
# that's what this test tests/demonstrates. | |
def test_defaulted(self): | |
@tuplifyarg('a') | |
def a_was_defaulted(*, a=None): | |
return a is None | |
# note/understand the difference being demonstrated here! | |
self.assertTrue(a_was_defaulted()) | |
self.assertFalse(a_was_defaulted(a=None)) | |
unittest.main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment