OK, here is the new version. You're the beta tester. The module and the decorator are now named usevarnames

# python 2 or 3
from __future__ import print_function
"""module usevarnames - decorator for magic assignment expression
"""
import sys
__version__ = '0.5.0'

if sys.version_info >=(3 ,):
    byte_ord = lambda x :x
else :
    byte_ord = ord

import collections
import functools
import opcode

CALL, UNPACK, STOREF, STOREN = (opcode.opmap[s] for s in
  ("CALL_FUNCTION", "UNPACK_SEQUENCE", "STORE_FAST", "STORE_NAME"))

class VarnamesError (Exception ): pass


def assignment_varnames (code, lasti):
    """Generate variable names extracted from a statement of the form
    x, y, z = function(...)
    in a code objet @code where @lasti is the index of the
    CPython bytecode instruction where the function is called.
    """
    errmsg ="simple assignment syntax 'x, y, z = ...' expected"
    co = code.co_code
    i =lasti
    if byte_ord(co[i]) != CALL: raise VarnamesError(errmsg)
    i += 3
    if byte_ord(co[i]) == UNPACK:
        nvars = byte_ord(co[i+1]) + byte_ord(co[i+2]) * 256
        i += 3
    else :nvars = 1
    for j in range(nvars):
        k = byte_ord(co[i])
        oparg = byte_ord(co[i+1]) + byte_ord(co[i+2]) * 256
        if k == STOREF: yield code.co_varnames[oparg]
        elif k == STOREN : yield code.co_names[oparg]
        else: raise VarnamesError(errmsg)
        i += 3

def usevarnames(func):
    """usevarnames(function) -> decorated function

    This decorator restricts the usage of a function
    to simple assignment statements such as

        x, y, z = function(...)

    A counterpart is that the function's body can
    use the function varnames() to obtain the assigned names.
    It means that within the body of the function, the
    expression

        varnames()

    will return the tuple ('x', 'y', 'z') of the variable
    names being assigned.

    The function must return a number of values corresponding
    to the number of variables being assigned. If there are
    two variables or more, the function can return any iterable
    with this number of arguments.
    """

    @functools.wraps(func)
    def wrapper (*args, **kwargs):
        f = sys._getframe(1)
        try:
            code ,lasti = f.f_code, f.f_lasti
        finally :
            del f
        names = tuple(assignment_varnames(code, lasti))
        _STACK.appendleft(names)
        try:
            return func(*args, **kwargs)
        finally:
            _STACK.popleft()
    return wrapper

_STACK = collections.deque()


def varnames():
    return _STACK[0]

if __name__  == "__main__":

    @usevarnames
    def attrof(obj):
        attrs = varnames()
        vals = tuple(getattr(obj, a) for a in attrs)
        if len(vals) > 1: return vals
        elif vals: return vals[0]

    import os, telnetlib

    Telnet = attrof(telnetlib)
    print(Telnet)
    mkdir, unlink, chdir = attrof(os)
    print(mkdir, unlink, chdir)
    print(_STACK)

your above code untouched:

Python 2.7.6 (default, Nov 10 2013, 19:24:18) [MSC v.1500 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> ================================ RESTART ================================
>>> 
>>> @usevarnames
def f(): print varnames(); return (0,1,2)

>>> a,b,c = f()
('a', 'b', 'c')
>>> 

this is a portable python interpreter ran on Wine 1.7 with an x86 prefix.

and here's my code:

import sys, functools, opcode

CALL, UNPACK, STOREF, STOREN = (opcode.opmap[s] for s in
  ("CALL_FUNCTION", "UNPACK_SEQUENCE", "STORE_FAST", "STORE_NAME"))
class VarnamesError (Exception ): pass
if sys.version_info >=(3,): ord = lambda x :x
def usevarnames(func):
    """usevarnames(function) -> decorated function
    This decorator restricts the usage of a function
    to simple assignment statements such as
        x, y, z = function(...)
    A counterpart is that the function's body can
    use the function varnames() to obtain the assigned names.
    It means that within the body of the function, the
    expression
        varnames()
    will return the tuple ('x', 'y', 'z') of the variable
    names being assigned.
    The function must return a number of values corresponding
    to the number of variables being assigned. If there are
    two variables or more, the function can return any iterable
    with this number of arguments.
    """
    @functools.wraps(func)
    def wrapper (*args, **kwargs):

        # --- get frame data ---
        f = sys._getframe(1)
        try: code, lasti = f.f_code, f.f_lasti
        finally : del f # prevent memory leak

        # --- initial setup ---
        errmsg = "simple assignment syntax 'x, y, z = ...' expected"
        varnames = []; co = code.co_code; i = lasti

        # --- validation ---
        if ord( co[i] ) != CALL: raise VarnamesError (errmsg )
        i+=3
        if ord( co[i] ) == UNPACK :
            nvars = ord( co[i+1] ) + ord( co[i+2] ) * 256
            i+=3
        else: nvars = 1

        # --- get results ---
        for j in range(nvars):
            k = ord( co[i] ); oparg = ord( co[i+1] ) + ord( co[i+2] ) * 256
            if k==STOREF: varnames.append( code.co_varnames[ oparg ] )
            elif k==STOREN: varnames.append( code.co_names[ oparg ] )
            else: raise VarnamesError( errmsg )
            i+=3

        # apply the results to the function and return the call
        func.func_globals['varnames'] = varnames
        return func( *args, **kwargs )
    return wrapper

just to clear up the leftover obfuscation, I've commented on the lines where needed.

and this code is better on performance
(due to python being a slow language, any little performance increase matters)

my output:

Python 2.7.6 (default, Nov 10 2013, 19:24:18) [MSC v.1500 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> ================================ RESTART ================================
>>> 
>>> @usevarnames
def f(): print varnames; return (0,1,2)

>>> a,b,c = f()
['a', 'b', 'c']
>>> 

thanks grib :)

btw, here's something else I wanted to test:

>>> a,b = f(),1

Traceback (most recent call last):
  File "<pyshell#2>", line 1, in <module>
    a,b = f(),1
  File "\home\tcll\Desktop\UMC\UMC_v3.0\API\externals.py", line 50, in wrapper
    else: raise VarnamesError( errmsg )
VarnamesError: simple assignment syntax 'x, y, z = ...' expected

kindof expected this to happen :P

Hm, now that we have the varnames() function, I don't see why we would need a decorator anymore. Why not directly examine the bytecode when varnames() is called ?

Here is the new version without decorator. I don't know why I didn't think about this in the first place.

# python 2 or 3
from __future__ import print_function
"""module usevarnames - support for magic assignment expression
"""
import sys
__version__ = '0.5.1'

if sys.version_info >=(3 ,):
    byte_ord = lambda x :x
else :
    byte_ord = ord

import opcode

CALL, UNPACK, STOREF, STOREN = (opcode.opmap[s] for s in
  ("CALL_FUNCTION", "UNPACK_SEQUENCE", "STORE_FAST", "STORE_NAME"))

class VarnamesError (Exception ): pass


def assignment_varnames (code, lasti):
    """Generate variable names extracted from a statement of the form
    x, y, z = function(...)
    in a code objet @code where @lasti is the index of the
    CPython bytecode instruction where the function is called.
    """
    errmsg ="simple assignment syntax 'x, y, z = func(...)' expected"
    co = code.co_code
    i =lasti
    if byte_ord(co[i]) != CALL: raise VarnamesError(errmsg)
    i += 3
    if byte_ord(co[i]) == UNPACK:
        nvars = byte_ord(co[i+1]) + byte_ord(co[i+2]) * 256
        i += 3
    else :nvars = 1
    for j in range(nvars):
        k = byte_ord(co[i])
        oparg = byte_ord(co[i+1]) + byte_ord(co[i+2]) * 256
        if k == STOREF: yield code.co_varnames[oparg]
        elif k == STOREN : yield code.co_names[oparg]
        else: raise VarnamesError(errmsg)
        i += 3

def varnames():
    """varnames() -> sequence of variable names

    This function can only be used inside the body of
    another function F which can only be used in a
    simple assignment statement such as

        x, y, z = F(...)

    It means that within the body of F, the
    expression

        varnames()

    will return the tuple ('x', 'y', 'z') of the variable
    names being assigned.

    The function must return a number of values corresponding
    to the number of variables being assigned. If there are
    two variables or more, the function can return any iterable
    with this number of arguments.

    The function F itself can not be wrapped in a decorator
    for this to work: when F() is called, the frame where
    the varnames() expressio is executed must be the next in
    the execution stack with respect to the frame executing
    the assignment statement.
    """
    f = sys._getframe(2)
    try:
        code ,lasti = f.f_code, f.f_lasti
    finally :
        del f
    return tuple(assignment_varnames(code, lasti))


if __name__  == "__main__":


    def attrof(obj):
        attrs = varnames()
        vals = tuple(getattr(obj, a) for a in attrs)
        if len(vals) > 1: return vals
        elif vals: return vals[0]

    import os, telnetlib

    Telnet = attrof(telnetlib)
    print(Telnet)
    mkdir, unlink, chdir = attrof(os)
    print(mkdir, unlink, chdir)

due to python being a slow language, any little performance increase matters

This is simply not true. It is true that python is a slow language, as compared to C for example, but optimisation must be very carefuly studied. Your optimisation here may yield a few microseconds at best, which are not the reason why your code is slow, and which certainly don't justify destroying the program's structure.

so I've finally gotten off my lazy duff and did some research and fixed a few things in your code:

tests:

t1 = f()
x,y,z = f()
t2,v = f(), 1
v,t3 = 1,f()
v,v2,t4 = 1,2,f()

results:

>>> 
STORE_NAME
['t1']
UNPACK_SEQUENCE
['x', 'y', 'z']
LOAD_CONST
['t2']
ROT_TWO
['t3']
ROT_THREE
['t4']
>>> 

the update:

# --- validation ---
if ord(co[i]) != CALL: raise VarnamesError(errmsg)
i += 3

o = ord(co[i])
print opcode.opname[o]
if o == UNPACK: nvars = ord(co[i+1]) + ord(co[i+2]) * 256; i += 3
elif o == LOADC: nvars = 1; i += 4
#need some work here:
elif o == ROT2: nvars = 1; i += 4
elif o == ROT3: nvars = 1; i += 8

there's still alot more to test:

v,(x,y,z) = 1,f()

here's an update to the code:

# --- validation ---
if ord(co[i]) != CALL: raise VarnamesError(errmsg)
i += 3

if ord(co[i]) == BUILDT: i += 3 # next call should be UNPACK, but we need name[-1] only
o = ord(co[i])
#print o
print '\n',opcode.opname[o]
if o == UNPACK: nvars = ord(co[i+1]) + ord(co[i+2]) * 256; i += 3
elif o in (LOADC,ROT2): nvars = 1; i += 4
elif o == ROT3: nvars = 1; i += 8

else: nvars = 1

my tests:

def f(): print varnames(); return (0,1,2)

t1 = f() # STORE_NAME
x,y,z = f() # UNPACK_SEQUENCE
t2,v = f(), 1 # LOAD_CONST
v,t3 = 1,f() # ROT_TWO
v,v2,t4 = 1,2,f() # ROT_THREE
# doesn't quite work properly:
v,v2,v3,t4 = 1,2,3,f() # BUILD_TUPLE - UNPACK_SEQUENCE
v,v2,v3,v4,t5 = 1,2,3,4,f() # BUILD_TUPLE - UNPACK_SEQUENCE
v,v2,v3,v4,v5,t6 = 1,2,3,4,5,f() # BUILD_TUPLE - UNPACK_SEQUENCE
v,v2,v3,v4,v5,v6,v7,v8,t7 = 1,2,3,4,5,6,7,8,f() # BUILD_TUPLE - UNPACK_SEQUENCE
# broken:
v,v23,t8,v3,v4 = 1,2,f(),3,4 # LOAD_CONST

results:

Python 2.7.6 (default, Nov 10 2013, 19:24:18) [MSC v.1500 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> ================================ RESTART ================================
>>> 

STORE_NAME
['t1']

UNPACK_SEQUENCE
['x', 'y', 'z']

LOAD_CONST
['t2']

ROT_TWO
['t3']

ROT_THREE
['t4']

UNPACK_SEQUENCE
['v', 'v2', 'v3', 't4']

UNPACK_SEQUENCE
['v', 'v2', 'v3', 'v4', 't5']

UNPACK_SEQUENCE
['v', 'v2', 'v3', 'v4', 'v5', 't6']

UNPACK_SEQUENCE
['v', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7', 'v8', 't7']

LOAD_CONST

Traceback (most recent call last):
  File "\home\tcll\Desktop\UMC\UMC_v3.0\API\externals.py", line 155, in <module>
    v,v23,t8,v3,v4 = 1,2,f(),3,4 # LOAD_CONST
  File "\home\tcll\Desktop\UMC\UMC_v3.0\API\externals.py", line 142, in f
    def f(): print varnames(); return (0,1,2)
  File "\home\tcll\Desktop\UMC\UMC_v3.0\API\externals.py", line 137, in varnames
    else: raise VarnamesError(errmsg)
VarnamesError: simple assignment syntax 'x, y, z = func(...)' expected
>>> 

't4' - 't7' should be just those vars.

EDIT: update:

BUILD_TUPLE = False
if ord(co[i]) == BUILDT: BUILD_TUPLE=True; i += 3 # next call should be UNPACK

...

return [names[-1]] if BUILD_TUPLE else names

results:

>>> 

STORE_NAME
['t1']

UNPACK_SEQUENCE
['x', 'y', 'z']

LOAD_CONST
['t2']

ROT_TWO
['t3']

ROT_THREE
['t4']

UNPACK_SEQUENCE
['t4']

UNPACK_SEQUENCE
['t5']

UNPACK_SEQUENCE
['t6']

UNPACK_SEQUENCE
['t7']

LOAD_CONST

Traceback (most recent call last):

more tests:

def f(): print varnames(); return (0,1,2)

t1 = f() # STORE_NAME
x,y,z = f() # UNPACK_SEQUENCE
t2,v = f(), 1 # LOAD_CONST
v,t3 = 1,f() # ROT_TWO
v,v2,t4 = 1,2,f() # ROT_THREE
v,v2,v3,t4 = 1,2,3,f() # BUILD_TUPLE - UNPACK_SEQUENCE
v,v2,v3,v4,t5 = 1,2,3,4,f() # BUILD_TUPLE - UNPACK_SEQUENCE
v,v2,v3,v4,v5,t6 = 1,2,3,4,5,f() # BUILD_TUPLE - UNPACK_SEQUENCE
v,v2,v3,v4,v5,v6,v7,v8,t7 = 1,2,3,4,5,6,7,8,f() # BUILD_TUPLE - UNPACK_SEQUENCE
[v for v in f()] # GET_ITER (unlikely for what I need)
# broken: (gets [] or something not expected)
v,(x,y,z) = 1,f() # ROT_TWO (expected ['x', 'y', 'z'])
v,v2,(x,y,z) = 1,2,f() # ROT_THREE (expected ['x', 'y', 'z'])
v,v2,v3,(x,y,z) = 1,2,3,f() # BUILD_TUPLE - UNPACK_SEQUENCE (expected ['x', 'y', 'z'] but got ['v3'])
v,v2,t8,v3,v4 = 1,2,f(),3,4 # LOAD_CONST (expected ['t8'])
v,v2,(x,y,z),v3,v4 = 1,2,f(),3,4 # LOAD_CONST (expected ['x', 'y', 'z'])
[s for s in [f()]] # BUILD_LIST (this should return ['s'])

also, I've commented out the exception in the for loop

#else: raise VarnamesError(errmsg)

EDIT:
added an extra print to the for loop,
here's what I get for the broken results:

ROT_TWO

UNPACK_SEQUENCE
[]


ROT_THREE

UNPACK_SEQUENCE
[]


UNPACK_SEQUENCE

STORE_NAME
STORE_NAME
STORE_NAME
UNPACK_SEQUENCE
['v3']


LOAD_CONST

BINARY_ADD
[]


LOAD_CONST

BINARY_ADD
[]


BUILD_LIST

BUILD_LIST
[]

EDIT2:
if you'd like to see my code as I actively edit it,
I'm editing UMC3.0's API-externals with this code :)
https://copy.com/StUO1g4m7gwuQyiD

this could prove to be very useful for UMC_SIDE, for any UMC/UGE data type that can be externally stored:

bigfloat5 = bf(5) # a 5-byte IEEE754 float

^ this can be highlighted in UMC_SIDE thanks to varnames() :)

yea, big time credit to you for this ;)

hey grib, I think I know what's wrong here...

I think we should use more of a state-engine setup (while True:) rather than the brute-force single-pass validation setup we're using now

we could then define defaults, which when tested, would append the proper values.
(it would also be much easier to manage)

The original idea was not to build a bytecode decompiler. Why do you want to support expressions such as

a, b, c, d = 1, f(), 2

When you define a struct with

foo = struct(...)

it is typically an isolated assignment expression, like a typedef struct in C. There is little benefit in being able to set another variable on the same line.

That doesn't mean somebody won't attempt it in a UMC/UGE script...
why? because it's python, and that sort of behavior is expected with variables.

so I've gotten it working, mostly: (other than the last 3)

Python 2.7.6 (default, Nov 10 2013, 19:24:18) [MSC v.1500 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> ================================ RESTART ================================
>>> 
['t1']
['x', 'y', 'z']
['t2']
['t3']
['t4']
['t5']
['t6']
['t7']
['t8']
[]
['x', 'y', 'z']
['x', 'y', 'z']
['x', 'y', 'z']
[]
[]
[]
>>> ================================ RESTART ================================
>>> 

STORE_NAME
LOAD_NAME
['t1']

UNPACK_SEQUENCE
STORE_NAME
STORE_NAME
STORE_NAME
['x', 'y', 'z']

LOAD_CONST
STORE_NAME
['t2']

ROT_TWO
STORE_NAME
['t3']

ROT_THREE
STORE_NAME
['t4']

BUILD_TUPLE
UNPACK_SEQUENCE
STORE_NAME
STORE_NAME
STORE_NAME
STORE_NAME
['t5']

BUILD_TUPLE
UNPACK_SEQUENCE
STORE_NAME
STORE_NAME
STORE_NAME
STORE_NAME
STORE_NAME
['t6']

BUILD_TUPLE
UNPACK_SEQUENCE
STORE_NAME
STORE_NAME
STORE_NAME
STORE_NAME
STORE_NAME
STORE_NAME
['t7']

BUILD_TUPLE
UNPACK_SEQUENCE
STORE_NAME
STORE_NAME
STORE_NAME
STORE_NAME
STORE_NAME
STORE_NAME
STORE_NAME
STORE_NAME
STORE_NAME
['t8']

GET_ITER
STOP_CODE
[]

ROT_TWO
UNPACK_SEQUENCE
STORE_NAME
STORE_NAME
STORE_NAME
['x', 'y', 'z']

ROT_THREE
UNPACK_SEQUENCE
STORE_NAME
STORE_NAME
STORE_NAME
['x', 'y', 'z']

BUILD_TUPLE
UNPACK_SEQUENCE
STORE_NAME
STORE_NAME
STORE_NAME
UNPACK_SEQUENCE
STORE_NAME
STORE_NAME
STORE_NAME
['x', 'y', 'z']

LOAD_CONST
BINARY_ADD
[]

LOAD_CONST
BINARY_ADD
[]

BUILD_LIST
GET_ITER
[]
>>> 

here's the code:

import sys, opcode

CALL, UNPACK, STOREF, STOREN, LOADC, ROT2, ROT3, BUILDT = (opcode.opmap[s] for s in
  ("CALL_FUNCTION", "UNPACK_SEQUENCE", "STORE_FAST", "STORE_NAME",
   "LOAD_CONST", "ROT_TWO", "ROT_THREE", "BUILD_TUPLE"))

class VarnamesError (Exception ): pass
if sys.version_info >=(3,): ord = lambda x :x
def varnames():
    """varnames() -> sequence of variable names

    This function can only be used inside the body of
    another function F which can only be used in a
    simple assignment statement such as

        x, y, z = F(...)

    It means that within the body of F, the
    expression

        varnames()

    will return the tuple ('x', 'y', 'z') of the variable
    names being assigned.

    The function must return a number of values corresponding
    to the number of variables being assigned. If there are
    two variables or more, the function can return any iterable
    with this number of arguments.

    The function F itself can not be wrapped in a decorator
    for this to work: when F() is called, the frame where
    the varnames() expressio is executed must be the next in
    the execution stack with respect to the frame executing
    the assignment statement.
    """

    # --- get frame data ---
    f = sys._getframe(2)
    try: code ,lasti = f.f_code, f.f_lasti
    finally: del f # prevent memory leak

    # --- initial setup ---
    co = code.co_code; i = lasti; BUILD_TUPLE = False
    names = []; nvars = 1

    # --- validation ---
    if ord(co[i]) != CALL: raise VarnamesError("varnames() not called properly in function")
    i += 3
    #print
    while True:
        o, oparg = ord(co[i]), ord(co[i+1]) + ord(co[i+2]) * 256
        #print opcode.opname[o]
        if o == BUILDT: BUILD_TUPLE=True
        elif o == UNPACK: nvars = oparg
        elif o in (LOADC,ROT2): i += 1
        elif o == ROT3: i += 5
        elif o == STOREF:
            if BUILD_TUPLE:
                if nvars==1: BUILD_TUPLE=False
            else: names.append( code.co_varnames[oparg] )
        elif o == STOREN :
            if BUILD_TUPLE:
                if nvars==1: BUILD_TUPLE=False
            else: names.append( code.co_names[oparg] )
        if not nvars: return names
        i += 3; nvars -= 1


#testing:
def f(): print varnames(); return (0,1,2)

t1 = f() # STORE_NAME
x,y,z = f() # UNPACK_SEQUENCE
t2,v = f(), 1 # LOAD_CONST
v,t3 = 1,f() # ROT_TWO
v,v2,t4 = 1,2,f() # ROT_THREE
v,v2,v3,t4 = 1,2,3,f() # BUILD_TUPLE - UNPACK_SEQUENCE
v,v2,v3,v4,t5 = 1,2,3,4,f() # BUILD_TUPLE - UNPACK_SEQUENCE
v,v2,v3,v4,v5,t6 = 1,2,3,4,5,f() # BUILD_TUPLE - UNPACK_SEQUENCE
v,v2,v3,v4,v5,v6,v7,v8,t7 = 1,2,3,4,5,6,7,8,f() # BUILD_TUPLE - UNPACK_SEQUENCE
[v for v in f()] # GET_ITER (unlikely for what I need, but works)
v,(x,y,z) = 1,f() # ROT_TWO - UNPACK_SEQUENCE
v,v2,(x,y,z) = 1,2,f() # ROT_THREE - UNPACK_SEQUENCE
v,v2,v3,(x,y,z) = 1,2,3,f() # BUILD_TUPLE - UNPACK_SEQUENCE - UNPACK_SEQUENCE
# broken: (gets [])
v,v2,t8,v3,v4 = 1,2,f(),3,4 # LOAD_CONST (expected ['t8'])
v,v2,(x,y,z),v3,v4 = 1,2,f(),3,4 # LOAD_CONST (expected ['x', 'y', 'z'])
[s for s in [f()]] # BUILD_LIST (this should return ['s'])

excuse the VarnamesError text
I'm assuming varnames() must be called before anything else is done in the function.

small update: (added NEXT)

# --- initial setup ---
co = code.co_code; i = lasti
BUILD_TUPLE = False; NEXT = False
names = []; nvars = 1

# --- validation ---
if ord(co[i]) != CALL: raise VarnamesError("varnames() not called properly in function")
i += 3
#print
#for t in range(i,i+21,3): print opcode.opname[ord(co[t])]
while True:
    o, oparg = ord(co[i]), ord(co[i+1]) + ord(co[i+2]) * 256
    #print opcode.opname[o]
    if o == BUILDT: BUILD_TUPLE=True
    elif o == UNPACK: nvars = oparg
    elif o == LOADC: NEXT = True
    elif o == ROT2: i += 1
    elif o == ROT3: i += 5
    elif o == STOREF:
        if BUILD_TUPLE:
            if nvars==1: BUILD_TUPLE=False
        else: names.append( code.co_varnames[oparg] )
    elif o == STOREN :
        if BUILD_TUPLE:
            if nvars==1: BUILD_TUPLE=False
        else: names.append( code.co_names[oparg] )
    if not nvars: return names
    i += 3
    if NEXT: NEXT = False
    else: nvars -= 1

results:

>>> 
['t1']
['x', 'y', 'z']
['v']
['t3']
['t4']
['t5']
['t6']
['t7']
['t8']
[]
['x', 'y', 'z']
['x', 'y', 'z']
['x', 'y', 'z']
['v4']
['z']
['v4']
[]

could I get a little help with the last 4 :/

tests:

v,v2,t9,v3,v4 = 1,2,f(),3,4 # LOAD_CONST (expected ['t9'])
v,v2,(x,y,z),v3,v4 = 1,2,f(),3,4 # LOAD_CONST (expected ['x', 'y', 'z'])
v,v2,v3,t10,v4 = 1,2,f(),3,4 # LOAD_CONST (expected ['t10'])
[s for s in [f()]] # BUILD_LIST (this should return ['s'])

I'm lost there... >_>

updated slightly: (better debug info and call validation (no exception))

import sys, opcode

if sys.version_info >=(3,): ord = lambda x :x
CALL, UNPACK, STOREF, STOREN, LOADC, ROT2, ROT3, BUILDT = (opcode.opmap[s] for s in
  ("CALL_FUNCTION", "UNPACK_SEQUENCE", "STORE_FAST", "STORE_NAME",
   "LOAD_CONST", "ROT_TWO", "ROT_THREE", "BUILD_TUPLE"))
def varnames():
    """varnames() -> sequence of variable names

    usage:

    # decorators not yet supported
    def F():
        ''' function description '''
        vars = varnames()

        # your code here
    """

    # --- get frame data ---
    # TODO: deal with the trace decorator for UMC_SIDE's data/stack viewers
    # (validate which frame our function is called on)
    f = sys._getframe(2)
    try: code ,lasti = f.f_code, f.f_lasti
    finally: del f # prevent memory leak

    # --- initial setup ---
    co = code.co_code; i = lasti
    valid = False; BUILD_TUPLE = False; NEXT = False
    names = []; nvars = 1

    # --- validation ---
    print
    #for t in range(i,i+21,3): print opcode.opname[ord(co[t])]
    while True:
        o, oparg = ord(co[i]), ord(co[i+1]) + ord(co[i+2]) * 256
        '''
        print opcode.opname[o], oparg,
        if o not in (BUILDT,UNPACK,CALL):
            try: print code.co_varnames[oparg]
            except:
                try: print code.co_names[oparg]
                except: print
        else: print
        '''#'''
        if not valid:
            if o == CALL: valid = True; NEXT = True
            else: return [] # don't stop the execution (just deal with it)

        if o == BUILDT: BUILD_TUPLE=True
        elif o == UNPACK: nvars = oparg
        elif o == LOADC: NEXT = True
        elif o == ROT2: i += 1
        elif o == ROT3: i += 5
        elif o == STOREF:
            if BUILD_TUPLE:
                if nvars==1: BUILD_TUPLE=False
            else: names.append( code.co_varnames[oparg] )
        elif o == STOREN :
            if BUILD_TUPLE:
                if nvars==1: BUILD_TUPLE=False
            else: names.append( code.co_names[oparg] )
        if not nvars: return names
        i += 3
        if NEXT: NEXT = False
        else: nvars -= 1

debugging info for my last 4 tests:

>>> 

CALL_FUNCTION 0
LOAD_CONST 6 trace
LOAD_CONST 21 t9
BUILD_TUPLE 5
UNPACK_SEQUENCE 5
STORE_NAME 19 v
STORE_NAME 20 v2
STORE_NAME 21 t9
STORE_NAME 22 v3
STORE_NAME 23 v4
['v4']

CALL_FUNCTION 0
LOAD_CONST 6 trace
LOAD_CONST 21 t9
BUILD_TUPLE 5
UNPACK_SEQUENCE 5
STORE_NAME 19 v
STORE_NAME 20 v2
UNPACK_SEQUENCE 3
STORE_NAME 24 x
STORE_NAME 25 y
STORE_NAME 26 z
['z']

CALL_FUNCTION 0
LOAD_CONST 6 trace
LOAD_CONST 21 t9
BUILD_TUPLE 5
UNPACK_SEQUENCE 5
STORE_NAME 19 v
STORE_NAME 20 v2
STORE_NAME 22 v3
STORE_NAME 27 t10
STORE_NAME 23 v4
['v4']

CALL_FUNCTION 0
BUILD_LIST 1 inspect
GET_ITER 3165
[]
>>> 

and I just realized the first LOAD_CONST which passed earlier was broken... heh

EDIT:
huh, after testing the first test again, I've noticed the values for LOAD_CONST change, though the initial remains the same:

>>> 

CALL_FUNCTION 0
LOAD_CONST 19 t2
ROT_TWO 4954
STORE_NAME 20 v
['v']

CALL_FUNCTION 0
LOAD_CONST 6 trace
LOAD_CONST 21 v2
BUILD_TUPLE 5
UNPACK_SEQUENCE 5
STORE_NAME 20 v
STORE_NAME 21 v2
STORE_NAME 22 t9
STORE_NAME 23 v3
STORE_NAME 24 v4
['v4']

CALL_FUNCTION 0
LOAD_CONST 6 trace
LOAD_CONST 21 v2
BUILD_TUPLE 5
UNPACK_SEQUENCE 5
STORE_NAME 20 v
STORE_NAME 21 v2
UNPACK_SEQUENCE 3
STORE_NAME 25 x
STORE_NAME 26 y
STORE_NAME 27 z
['z']

CALL_FUNCTION 0
LOAD_CONST 6 trace
LOAD_CONST 21 v2
BUILD_TUPLE 5
UNPACK_SEQUENCE 5
STORE_NAME 20 v
STORE_NAME 21 v2
STORE_NAME 23 v3
STORE_NAME 28 t10
STORE_NAME 24 v4
['v4']

CALL_FUNCTION 0
BUILD_LIST 1 inspect
GET_ITER 3165
[]
>>> 

and of course I'm still lost :P

I'll have a look.

I modified the code of dis.disassemble() to create a function which disassembles the line where the function f() is called. I think it can help you in trying to handle complex assignment statements. Here is the code

#!/usr/bin/env python
# -*-coding: utf8-*-
"""essais de désassemblage de code"""
from __future__ import (absolute_import, division,
                        print_function, unicode_literals)

import binascii as bi
from collections import namedtuple
import dis
import sys

from dis import (findlabels, findlinestarts)

record = namedtuple("record",
         "line lasti label index op opname oparg has value")

def record_disassemble(co, lasti=-1, current_line=False):
    """Disassemble a code object."""
    code = co.co_code
    labels = findlabels(code)
    L = list(findlinestarts(co))
    linestarts = dict(L)
    if current_line:
        if lasti < 0:
            raise ValueError("Bytecode index expected (lasti).")
        M = [p for p in L if p[0] <= lasti]
        i = M[-1][0]
        n = L[len(M)][0] if len(M) < len(L) else len(code)
    else:
        n = len(code)
        i = 0
    extended_arg = 0
    free = None
    while i < n:
        args = []
        app = args.append
        c = code[i]
        op = ord(c)
        if i in linestarts:
            app(linestarts[i])
        else:
            app(None)

        if i == lasti: app(True)
        else: app(False)
        if i in labels: app(True)
        else: app(False)
        app(i)
        app(op)
        app(dis.opname[op])
        i = i+1
        if op >= dis.HAVE_ARGUMENT:
            oparg = ord(code[i]) + ord(code[i+1])*256 + extended_arg
            extended_arg = 0
            i = i+2
            if op == dis.EXTENDED_ARG:
                extended_arg = oparg*65536L
            app(oparg)
            if op in dis.hasconst:
                app('const')
                app(co.co_consts[oparg])
            elif op in dis.hasname:
                app('name')
                app(co.co_names[oparg])
            elif op in dis.hasjrel:
                app('jrel')
                app(i + oparg)
            elif op in dis.haslocal:
                app('local')
                app(co.co_varnames[oparg])
            elif op in dis.hascompare:
                app('compare')
                app(cmp_op[oparg])
            elif op in dis.hasfree:
                app('free')
                if free is None:
                    free = co.co_cellvars + co.co_freevars
                app(free[oparg])
            else:
                args.extend((None, None))
        else:
            args.extend((None, None, None))
        yield record(*args)

def dissect_calling_line():
    f = sys._getframe(2)
    try:
        code, lasti, ln = f.f_code, f.f_lasti, f.f_lineno
    finally:
        del f
    return list(record_disassemble(code, lasti=lasti, current_line=True))



if __name__ == '__main__':

    def f(x, foo='bar'):
        for rec in dissect_calling_line():
            print(rec)
        sys.exit(0)

    class A(object):
        pass

    L = list()
    A.f = f
    a = A()

    v,L[2],t8,v3,v4 = 1,2,a.f(foo=2),3,4

""" my output -->
record(line=109, lasti=False, label=False, index=226, op=100, opname='LOAD_CONST', oparg=16, has=u'const', value=1)
record(line=None, lasti=False, label=False, index=229, op=100, opname='LOAD_CONST', oparg=17, has=u'const', value=2)
record(line=None, lasti=False, label=False, index=232, op=101, opname='LOAD_NAME', oparg=24, has=u'name', value='a')
record(line=None, lasti=False, label=False, index=235, op=106, opname='LOAD_ATTR', oparg=19, has=u'name', value='f')
record(line=None, lasti=False, label=False, index=238, op=100, opname='LOAD_CONST', oparg=18, has=u'const', value='foo')
record(line=None, lasti=False, label=False, index=241, op=100, opname='LOAD_CONST', oparg=17, has=u'const', value=2)
record(line=None, lasti=True, label=False, index=244, op=131, opname='CALL_FUNCTION', oparg=256, has=None, value=None)
record(line=None, lasti=False, label=False, index=247, op=100, opname='LOAD_CONST', oparg=19, has=u'const', value=3)
record(line=None, lasti=False, label=False, index=250, op=100, opname='LOAD_CONST', oparg=20, has=u'const', value=4)
record(line=None, lasti=False, label=False, index=253, op=102, opname='BUILD_TUPLE', oparg=5, has=None, value=None)
record(line=None, lasti=False, label=False, index=256, op=92, opname='UNPACK_SEQUENCE', oparg=5, has=None, value=None)
record(line=None, lasti=False, label=False, index=259, op=90, opname='STORE_NAME', oparg=25, has=u'name', value='v')
record(line=None, lasti=False, label=False, index=262, op=101, opname='LOAD_NAME', oparg=23, has=u'name', value='L')
record(line=None, lasti=False, label=False, index=265, op=100, opname='LOAD_CONST', oparg=17, has=u'const', value=2)
record(line=None, lasti=False, label=False, index=268, op=60, opname='STORE_SUBSCR', oparg=None, has=None, value=None)
record(line=None, lasti=False, label=False, index=269, op=90, opname='STORE_NAME', oparg=26, has=u'name', value='t8')
record(line=None, lasti=False, label=False, index=272, op=90, opname='STORE_NAME', oparg=27, has=u'name', value='v3')
record(line=None, lasti=False, label=False, index=275, op=90, opname='STORE_NAME', oparg=28, has=u'name', value='v4')
record(line=None, lasti=False, label=False, index=278, op=110, opname='JUMP_FORWARD', oparg=0, has=u'jrel', value=281)
"""

The result is a list of bycode instructions executed on the line where the function f() is called. The task is to extract the list of variables from this bytecode instructions. IMHO, a way to do it would be to simulate the behaviour of the python interpreter on that line. There is some work involved if you want a non trivial result.

I'll look into that and make it work as expected :)
don't think it can logically get any better than that ;)
thanks :D

Here is a starting point for a mock interpreter. It is far from complete but it already parses many examples. (It can't parse examples with list comprehension, but I don't think it is a good idea anyway).

I will try to complete the interpreter to handle more complex expressions in the assignment statement, and also parameters in function f. This is a very alpha code.

ah alright thanks, I know a few people who like to use lists like that... heh
so that's why I was using that particular example... heh

it's unlikely, though we can't completely out-rule it's use ;)
despite the fact it's bad practise.

Hey tcll, here is my latest version. There are probably some bugs which you can discover by testing but I think it can handle pretty complex assignment statements. It is for python 2.7

There is plenty of room to optimize it later if it works :)

sweet, I'll get right on that when I'm not working on my HexEdit template for the HAL_DAT model format =)

gotta love complex pathfinder logic... haha XP

but yea, I'll try out a few things that might be used in UMC-libraries ;)
(such as class redirects and such)

UMC-libs are shared between UMC-scripts and have the entire frontend and various parts of the backend to work off of...
blah blah

hey grib, your code is genius <3

though I have a few questions for a few other specific things

most importantly for this though, can it do multi-line??

a,b,c,d\
= 0,1,f(),2

or

a,b,c,d = (
0,1,f(),2 )

this functionality will be needed for UMC's new automation practices.
("automation" is basically functional programming using as little code as possible by supplying function calls as arguments for outer functions)

minor automation example:

vert = struct(12, 'x,y,z',
    x = bf32, # take note we're not calling bf32()
    y = bf32,
    z = bf32 )

# Begin basic automation using a UMC-defined array:
ugeSetVertArr( array( vert )( bu32() ) )

the actual "automation" starts when bu32() is hit which tells the array to read a count of vert structs and pass it directly to UMC's backend.
I'm working on more advanced methods to be able to use UMC-defined structs and set data types to further ease automation.

meaning you should be able to import a full model using only a single line of code :)

oh btw, I have a way to get rid of the order portion so a struct can behave more like it should.
when the struct is defined, have it analize it's internal order using your disassembler to get the internal variable names.

version conflicts are considered, just use None instead of a data type ;)

anyways, another thing I'd like to ask, I've kinda just stated what I wanted it for... lol
can your code be used for gathering more than just variable names??

for example, gathering the names, the function name, and the data it returns, at the offset it's called at in the code.

this would be something for SIDE, which needs to know all of that and then some when hitting the Go button which interactively parses an imported file using the script you're working on.

it needs the data for:
- highlighting errors in your script
- building the data tree (naming the data after your vars and displaying the data types)
- highlighting the file data structures after the data type colors

data type colors: (dynaically generated from the supplied base data types)
http://lh3.ggpht.com/-FQPLL35xDjU/U44jyPA6AaI/AAAAAAAAGrU/KUrYFUnYZeA/s1152/UMC_SIDE_progress_20.PNG
the data type bu32 gets it's color from the hash of it's base type name bu_(4) which comes from the dynamic class defined in UMC's backend.

NOTE: if you define your own data type reference in your script such as:
bu4 = bu(4)
bu4 will also be highlighted the same color as bu32, buword and bu(4) as they're all the same base type (bu(4) only returns the base type)

dynamic type definition is also considered bf(5) (5-byte floats) are not defined by default in UMC's backend.
once defined, the type is added to the backend and any references to the type are highlighted in SIDE.

my IDE is built for noobs, so I'm putting in as much as I can to cover as many "links" as possible :)

yah, I'm going deeper than VS2010 on this stuff :P
I want to go as far as having SIDE actively suggest a better way to write your code when you're doing something inefficient.

good for noobs and pros ;)

anyways, thanks :)

EDIT:
btw, your code helped me see alot of logic I never would've even guessed on. ;)
that's why I called it genius ;D

taught me alot about building a very basic interpreter, which I could put to use with UGESL.

many thanks :)

I have found 1 or 2 bugs in the last version. Here is a new version.

The code currently cannot be applied for multiline because it does not detect the beginning of the logical line of code, which means that in your example above, it will see the opcodes for line 2 but not for line 1. I need to add a mechanism to obtain the logical line.

I think I will upload a version in github soon, so that you can easily follow the changes in this code, the problem being that I don't have a lot of time to work on this now, so be patient...

EDIT: About obtaining the function name, you can get it from something similar to the printat() snippet https://www.daniweb.com/software-development/python/code/479747/print-with-line-and-file-information . Look at chriswelborn's comment.

commented: your welcome ;) +4

the problem being that I don't have a lot of time to work on this now, so be patient...

I have all the patience in the world, please, don't let me push you do do anything. ;)

also, I use copy for a synced development environment.
I can easily restore code if anything breaks. :)

as far as following changes, knowing me with GH, it'd be the same... heh
(GH is better at tracking changes similar to a wiki, but I'm not sure how to get my hands on that functionality) :P
^I'm sure copy will catch up... would you like an invite for +5GB?? :)

it does not detect the beginning of the logical line of code

I could make it search for it, though it might not be very friendly when it comes to real-time behavior.
(the whole interactive thing with SIDE and all)

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.