Skip to content

Instantly share code, notes, and snippets.

@outofmbufs
Created November 19, 2022 22:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save outofmbufs/719768ade2b92d85f47665ff80c9af56 to your computer and use it in GitHub Desktop.
Save outofmbufs/719768ade2b92d85f47665ff80c9af56 to your computer and use it in GitHub Desktop.
Python decorator to call a function and omit arguments that need to take on their default values, but only sometimes, so they want to be present in the call syntax but sometimes should be conditionally defaulted (useful if the function sets a non-trivial default value)
import functools
# Given a function such as this:
#
# def example(a, b=17):
# pass
#
# If code needs to conditionally call example() sometimes with b supplied
# and sometimes with b defaulted, that ends up looking like this:
#
# if some_condition::
# example(a, 42)
# else:
# example(a)
#
# assuming the caller does not want to duplicate/"know" that the default
# value for the b argument is 17.
#
# This decorator provides an alternate way to do that.
#
# @optionalarg
# def example(a, b=17):
# pass
#
# creates a sentinel value example.noarg so now the code can be written:
# if some_condition:
# b = 42
# else
# b = example.noarg
#
# example(a, b)
#
# Whether this is an improvement depends on the particulars of the
# code; obviously in this trivial example it doesn't make the code better.
#
# NOTE VERY CAREFULLY that example.noarg is a SENTINEL value and not
# the actual default value (17 in this example). E.g.:
#
# print(example.noarg)
#
# will output something like: <object object at 0x1021307d0>
#
# Sometimes the calling code may wish to designate its own
# sentinel value to mean "use the default"; None is common for this.
# The decorator can be called with a noarg keyword argument:
#
# @optionalarg(noarg=None)
# def example(a, b=17):
# print(b)
#
# example(1, None)
#
# will print 17, as "None" becomes the sentinel for "use the default for b".
# NOTE: In this example, there is no way to force b to be None in the
# underlying example() function (as None has been usurped to be the sentinel
# that implies "supply no argument" instead). Caveat coder.
#
_NOARG = object()
def optionalarg(f_or_nothing=_NOARG, /, *, noarg=_NOARG):
# turn 'naked' @args_decorator into @args_decorator()
# follow this carefully; it eventually returns _wrapped
if f_or_nothing is not _NOARG:
return optionalarg()(f_or_nothing)
def _deco(f):
@functools.wraps(f)
def _wrapped(*args, **kwargs):
return f(*(a for a in args if a is not noarg),
**{k: v for k, v in kwargs.items() if v is not noarg})
_wrapped.noarg = noarg
return _wrapped
return _deco
if __name__ == "__main__":
import unittest
class TestMethods(unittest.TestCase):
def test_nakedec(self):
@optionalarg
def example(a, b=17):
return a, b
for args, rv in (
((1, 2), (1, 2)),
((1,), (1, 17)),
((1, example.noarg), (1, 17))):
with self.subTest(args=args, rv=rv):
self.assertEqual(example(*args), rv)
def test_nakedeckw(self):
@optionalarg
def example(*, a, b=17):
return a, b
for kwargs, rv in (
(dict(a=1, b=2), (1, 2)),
(dict(a=1), (1, 17)),
(dict(a=1, b=example.noarg), (1, 17))):
with self.subTest(kwargs=kwargs, rv=rv):
self.assertEqual(example(**kwargs), rv)
def test_noargargument(self):
sentinel = object()
@optionalarg(noarg=sentinel)
def example(a, b=17):
return a, b
for args, rv in (
((1, 2), (1, 2)),
((1,), (1, 17)),
((1, sentinel), (1, 17))):
with self.subTest(args=args, rv=rv):
self.assertEqual(example(*args), rv)
def test_noargargumentkw(self):
sentinel = object()
@optionalarg(noarg=sentinel)
def example(*, a, b=17):
return a, b
for kwargs, rv in (
(dict(a=1, b=2), (1, 2)),
(dict(a=1), (1, 17)),
(dict(a=1, b=sentinel), (1, 17))):
with self.subTest(kwargs=kwargs, rv=rv):
self.assertEqual(example(**kwargs), rv)
def test_mixed(self):
@optionalarg
def vargs(b=17, *args, clown='bozo', **kwargs):
return (b, clown, args, kwargs)
self.assertEqual(vargs(), (17, 'bozo', (), dict()))
self.assertEqual(vargs(9), (9, 'bozo', (), dict()))
self.assertEqual(vargs(9, 99), (9, 'bozo', (99,), dict()))
self.assertEqual(vargs(clown=None), (17, None, (), dict()))
self.assertEqual(vargs(9, clown=None), (9, None, (), dict()))
self.assertEqual(vargs(9, clown=vargs.noarg),
(9, 'bozo', (), dict()))
self.assertEqual(vargs(9, 6, 666, clown=vargs.noarg, foo='bar'),
(9, 'bozo', (6, 666), dict(foo='bar')))
def test_mixed2(self):
xyzzy = object()
@optionalarg(noarg=xyzzy)
def vargs(b=17, *args, clown='bozo', **kwargs):
return (b, clown, args, kwargs)
self.assertTrue(vargs.noarg is xyzzy)
self.assertEqual(vargs(), (17, 'bozo', (), dict()))
self.assertEqual(vargs(9), (9, 'bozo', (), dict()))
self.assertEqual(vargs(9, 99), (9, 'bozo', (99,), dict()))
self.assertEqual(vargs(clown=None), (17, None, (), dict()))
self.assertEqual(vargs(9, clown=None), (9, None, (), dict()))
self.assertEqual(vargs(9, clown=vargs.noarg),
(9, 'bozo', (), dict()))
self.assertEqual(vargs(9, 6, 666, clown=xyzzy, foo='bar'),
(9, 'bozo', (6, 666), dict(foo='bar')))
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment