Javascript-esque Unpacking

starstar.unpack(data=None, *pos_defaults, _up_=0, _default_None_=True, _cached_assignment_=True, **defaults) tuple[source]

Javascript-esque dict unpacking! Don’t tell me you haven’t wished this was possible before.

Here’s how you can do it with Javascript ES6

const d = { a: 1, x: 1, y: 2 }
const { a, b: 5, ...c } = d

And here’s the closest possible equivalent in Python (without hacking the literal grammar lol)!

d = { 'a': 1, 'x': 1, 'y': 2 }

a, b = starstar.unpack(d, b=5)
assert (a, b) = (1, 5)

# approximating the js spread operator
a, b, *(c,) = starstar.unpack(d, b=5)
assert (a, b, c) = (1, 5, { 'x': 1, 'y': 2 })

The reason you need to use *(c,) and not just *c is because python will always assign a list to c when using the star operator, so instead we return [{...}] and therefore we need to do a quick destructure to get the dict from the single element list.

Parameters:
  • data (dict, iterable) – The data to unpack. Most commonly this is a dict although it also works for iterables. Unpacking iterables is already possible, but this allows you to use default values.

  • *pos_defaults (any) – Default values (correspond to unpacked position).

  • **defaults (any) – Named default values.

# unpack a dictionary

d = {'a': 5, 'b': 6}

a, b, c = unpack(d)
assert (a, b, c) == (5, 6, None)

a, b, c = unpack(d, c=10)
assert (a, b, c) == (5, 6, 10)

a, b, c = unpack(d, 1, 2, 3)
assert (a, b, c) == (5, 6, 3)

a, b, c = unpack({'a': 5}, 1, 2, 3)
assert (a, b, c) == (5, 2, 3)

# unpack a list

a, b, c = unpack([5, 6], 1, 2, 3)
assert (a, b, c) == (5, 6, 3)

How is this possible ????

Basically we:

  • parse the line(s) of code from the stack frame

  • crawl the preceeding lines of code until we find the beginning of the assignment. For multi-line statements, this is done by finding the assignment ) = ( and continuing upward until the parentheses are balanced.

  • use ast to parse the left side of the assignment into a (nested) tuple of strings matching the format of the assignment.

  • cache the assignment tuple using the frame’s filename and lineno for repeat calls.

  • use that nested tuple to pull out values from the dict/iterable in the format of the assignment, falling back to the supplied defaults.

Conditions needed for this to work:

  • the left side must be a single assignment i.e. a, b, c = unpack({...}), not x, y = a, b, c = unpack({...})

  • the right side must be a single assignment i.e. a, b = unpack({...}), not x, y = (a, b), c = unpack({...}), 2

  • don’t use backslashes for in-statement line breaks. I could technically fix it to support this, but honestly I’m not sure I want to condone such behavior anyways lol (just use parentheses !!)

starstar.assignedto(up=0, cache=True) tuple[source]

Parse the assignment of a line of code and get the names of the assignment variables.

Parameters:
  • up (int) – How many frames should we go up? If you wrap this function, it’s recommended to pass in _up + 1 where _up=0 is an arguments that callers can provide if they want to wrap your function.

  • cache (bool) – This can be (slightly) expensive especially if called repeatedly in a critical bit of code. By default, we cache the result of this value using the frame filename and line number. I realize this could break in some (?) situations so you have the ability to disable as needed. Keep in mind, when run repeatedly over time, it’s about 26x slower than using caching (tested using 100k iters).

Returns:

This will assign the names of the variables used for the assignment.

Return type:

(str, tuple)

Note

This is less useful used on its own, and is more useful when used inside of wrapper functions. See unpack() for a real example.

# simple call

x, y, z = assignedto()
assert (x, y, z) == ('x', 'y', 'z')

x, y, (a, (b, c)) = assignedto()
assert (x, y, z, a, b, c) == ('x', 'y', 'z', 'a', 'b', 'c')

# nested call

def asdf(_up=0):
    return assignedto(_up+1)

x, y, z = asdf()
assert (x, y, z) == ('x', 'y', 'z')

def asdf_flipped(_up=0):
    keys = assignedto(_up+1)
    return keys[::-1]

x, y, z = asdf_flipped()
assert (x, y, z) == ('z', 'y', 'x')

Use it to define constants:

OPEN, CLOSE = assignedto()

something = 'OPEN'
if something == OPEN: ...

Or internal symbols:

def tokens(_up=0):
    keys = assignedto(_up+1)
    return deep_apply(keys, '||| token: {} |||'.format)

a, b = tokens()
assert (a, b) == ('||| token: a |||', '||| token: b |||')