I don't claim this is useful nor that it adheres to best practices. There's also no good reason the hack is a context manager, and it won't work on the interactive shell.
__init__.py
from .context_manager import FrameHack
from .varname_setter import setvarname
from . import varnamespace
context_manager.py
from re import findall, split
from .instantiation_parser import assemble_cm, walk_f_back
class T:
    def __init__(self):
        pass
class TraceError(Exception):
    pass
class FrameHack:
    no = 0
    def __init__(self, *_):
        self.call_re = f'{self.__class__.__name__}\((.+)\).*'
        self.sep_re = '\s*,\s*'
        self.t = T()
    def __mul__(self, code):
        return self.call_re * code.count(self.__class__.__name__)
    def advance_pos(self, varnames):
        if isinstance(varnames, tuple):
            return len(varnames), varnames[self.__class__.no]
        else:
            return 1, varnames
    def setattrs(self, t, varnames, f_back):
        for name in split(self.sep_re, varnames.strip(', ')):
            if name.startswith('*'):
                for unpacked_name in walk_f_back(f_back, name.strip('* ')):
                    setattr(t, unpacked_name, unpacked_name)
            else:
                setattr(t, name, name)
    def normalize_pos(self, no):
        if self.__class__.no == no - 1:
            self.__class__.no = 0
        else:
            self.__class__.no += 1
    def __enter__(self):
        try:
            raise TraceError('Trace from raise')
        except Exception as exc:
            f_back = exc.__traceback__.tb_frame.f_back
            with open(f_back.f_code.co_filename) as f:
                code = assemble_cm(f.readlines(), f_back.f_lineno - 1)
            try:
                no, varnames = self.advance_pos(findall(self * code, code).pop())
            except IndexError as exc:
                raise TraceError('No arguments were passed to the constructor') from exc
            else:
                self.setattrs(self.t, varnames, f_back)
                self.normalize_pos(no)
                return self.t
    def __exit__(self, *_):
        for name in vars(self.t).copy():
            delattr(self.t, name)
        return False
Could not use contextlib.contextmanager on a function since co_filename would be the contextlib module so rolled with a class that allows keeping count of how many times the context manager was called between the with statement and the terminating colon.
Next module accounts for calls stretching across multiple lines.
instantiation_parser.py
UNWANTED = {ord(char): '' for char in '\\\r\n'}
def yield_ins(sequence, substring):
    line = ''
    sequence = iter(sequence)
    while substring not in line:
        line = next(sequence)
        yield line
def rev_readlines(startswith, code, f_lineno):
    if startswith not in code[f_lineno]:
        return list(yield_ins(reversed(code[:f_lineno]), startswith))[::-1]
    else:
        return []
def readlines(endswith, code, f_lineno):
    return list(yield_ins(code[f_lineno:], endswith))
def rm_unwanted(code):
    return code.translate(UNWANTED).strip()
def assemble_cm(*args):
    return ''.join(map(rm_unwanted, rev_readlines('with', *args) + readlines(':', *args)))
def walk_f_back(f_back, packed):
    unpacked = [unpacked_name for unpacked_name, v in f_back.f_locals.items() if v in f_back.f_locals[packed]]
    if bool(unpacked) is True:
        return unpacked
    else:
        return walk_f_back(f_back.f_back, packed)
walk_f_back deals with this scenario:
hello, world = 0, 1
packed = hello, world    
It will return the name of the first variable(s) whose value matches one in f_back.f_locals[packed]. I deem this acceptable. My goal was to create a sort of tunnel up the frames, bound to a variable name. It can be intercepted.
test.py
from unittest import TestCase, main
from . import FrameHack
from .context_manager import TraceError
from . import setvarname
class Test(TestCase):
    def test_multiline_nested(self):
        w, x, y, z = 0, 1, 2, 3
        _, *xy, _ = w, x, y, z
        with FrameHack(
                w, x
        ) as t0:
            with FrameHack \
                        (y
                        , z) as t1, FrameHack(
                *xy,
                w) as t2, FrameHack(w, z) as t3:
                self.assertEqual(vars(t0), {'x': 'x', 'w': 'w'})
                self.assertEqual(vars(t1), {'y': 'y', 'z': 'z'})
                self.assertEqual(vars(t2), {'x': 'x', 'y': 'y', 'w': 'w'})
                self.assertEqual(vars(t3), {'w': 'w', 'z': 'z'})
        self.assertTrue(vars(t0) == vars(t1) == vars(t2) == vars(t3) == {})
    def test_x_function(self):
        coro = setvarname()
        def func0(*args):
            var0, var1 = 10, 20
            with FrameHack(var0, *args, var1) as t:
                coro.send((t.x, 30))
                coro.send((t.y, 40))
                return vars(t).copy()
        def func1(*args):
            return func0(*args)
        def func2():
            w, x, y, z = 0, 1, 2, 3
            args = w, x, y, z
            return func1(*args)
        self.assertEqual(func2(), {'var0': 'var0', 'x': 'x', 'y': 'y', 'z': 'z', 'w': 'w', 'var1': 'var1'})
        from .varnamespace import x, y
        self.assertEqual(x + 10, y)
        self.assertEqual(coro.send(True), 0)
        coro.close()
    def test_indexerror(self):
        exc = ''
        try:
            with FrameHack() as _:
                pass
        except TraceError as trace_exc:
            exc = str(trace_exc)
        finally:
            self.assertEqual(exc, 'No arguments were passed to the constructor')
if __name__ == '__main__':
    main()
The unittests exemplify usage.
The following module is a work around for Python3's limitation on exec('var = 0') when used on local scopes. Set a variable by name on an empty module, from ... import ... the variable back in some other scope and you have a variable and not an attribute.
varname_setter.py
from . import varnamespace
def prime_coro(func):
    def deco(*args, **kwargs):
        coro = func(*args, **kwargs)
        next(coro)
        return coro
    return deco
@prime_coro
def setvarname(module=varnamespace):
    clean = module.__dict__.copy()
    cached = set()
    while True:
        var = yield len(cached)
        if var is True:
            for varname in cached.copy():
                module.__dict__.pop(varname)
                cached.remove(varname)
        elif var[0] in clean:
            continue
        else:
            setattr(module, *var)
            cached.add(var[0])
The coroutine yields back how many attributes were set in order to keep track of how dirty it's become and clean if necessary.
varnamespace.py -> empty