Collect python statement results which are not assigned

Hi Pythoneers,

I got python embedded into OpenSCAD and i’d like to make more use out of it.
statements like these do exactly what they should:

width= 3*5
solid = make_nice_solid(width)
other_solid = solid.size(1)
print(" Everything fine")

But i’d like to be able to write lines like these: These are expressions which create a value,
which is apparently NOT stored in a variable

solid *3
other_solid - solid

instead of wasting them, I’d like to collect them in an array and display send it to the display engine
after python evaluation has finished.
Is there some way how i can collect these orphaned expressions for later use?

You can overload the operator dunders, __add__, __mul__, etc. to log their inputs.

You could just make a list of them as they get calculated:

expressions = []

expressions.append(solid*3)
expressions.append(other_solid - solid) 

But if you don’t give them a name you’re not going to have to “remember” the order in which you created them.

(Perhaps you can explain the full use case a bit better?)

Hi Andrew

Yes, I understand the idea!
But in this case I am especially lazy , I don’t want to have this explicit array and want python to do this task for me.
Background is:
in original openscad you can also write:

cube();

here the result is also considered without any variable assignment.

@gsohler, in the following example statement, would width * 2 + 4 be considered a result that was not assigned and that you wish to catch?:

solid = make_nice_solid(width * 2 + 4)

While the result was passed to a function and presumably used to do some work, it was not assigned to a variable. For clarification, perhaps we need to be provided with some additional examples of statements that contain expressions that need to be caught.

you are right , I did not express myself good enough

I am interested in expressions which are not reused. this means neighter assigned nor passed to a function.
in the example :

‘’‘’
solid = make_nice_solid(width * 2 + 4)
‘’’

I don’t want to catch anything

I am basically interested in Statement Results on the stack toplevel which are created and could be immediately fed to the garbage collector, because nothing makes use of them

I am not exactly sure if print also returns something, but in case it did, this is something I want to collect

@gsohler Is this mean to be happening with a python script/program (i.e., a *.py file)? This would have to be done via some magic – it is not something that python does automatically (and probably not a good idea, honestly!).

But if you mean this to be happening at the command line, then if you run under ipython or jupyter, all of the individual results can be collected and accessed later as an array Out[] (but then you would have to be careful to put each into its own cell).

The print function always returns the value None, regardless of what is output. Nothing needs to make use of it. However, is that really something you would need to collect?

If garbage collection is a concern, there’s actually nothing to worry about; if a value is simply abandoned, the garbage collector will do its job without your having to do anything to invoke it.

basically yes: But i will discard it right after because i only want to use collected data from PyOpenSCAD Type

By that, do you mean the None that is returned by the print function? Note that quite a number of built-in functions return None. You may wind up with a collection that contains lots of None values. :slight_smile:

In Python itself, as you know, those returned values are just dropped since they’re not assigned to anything. But you could do this perhaps by defining your own REPL keeping track of the value of any expression (if not None). So, you’d have to add a little layer to your embedded Python to go through this slightly modified REPL. Only the exec function needs to be overridden, and you only need to do something special for non-assignments (which can be detected most easily probably by using ast.parse), since assignments are already cached in the globals dictionary.

$ ipython3
Python 3.11.5 (main, Aug 24 2023, 15:09:45) [Clang 14.0.3 (clang-1403.0.22.14.1)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.16.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: x=2

In [2]: y=7

In [3]: x*y
Out[3]: 14

In [4]: 1+x
Out[4]: 3

In [5]: 37.5
Out[5]: 37.5

In [6]: size=137

In [7]: print([val for val in Out.values() if val is not None])
[14, 3, 37.5]

This is actually a nice solution. Unfortunately i cannot use it, as i dont use ipython, but embedded python instead(libpython). Still thank you

Hans, this sounds very interesting. I feel that i need something like that.
As mentioned before, i use embedded python linking to libpython.
Actually I call PyRun_String somewhere in my program.
How can I overwrite the exec function ? Do I need to dynamically load libpython using dlopen,
or are there easier ways ?
Is it possible to provide me with little more detailled strategy ?

sys.displayhook might be an option, too, depending on how the script is run.

The strategy is actually exactly the same one that iPython (or Jupyter) is also following in its REPL.
You could either embed this in the C-code or perhaps as a pure-Python “shell” which you would always load in your embedded code.
As pure-Python code you could do sth like this (I’m not adding much error checking here, so you might want to make this more robust):

import ast
Out = dict()
def my_exec(stm, globals=None, locals=None, /):
      try:
          ast.parse(stm, mode="eval")
       except SyntaxError:  # is triggered by assignments
           # may want to double-check other possible exceptions - off the top of my head
           # I don't know if this code is sufficient as-is
           return exec(stm, globals, locals)
       n = len(Out)
       stm = f"_t{n} = {stm}"
       return exec(stm, globals, locals)

And you would then call this always with locals=Out.
So, you’re full shell would simply be running this extra REPL in the normal Python interpreter:

  • read input
  • call my_exec, instead of builtin exec

Don’t know how iPython implements this - So it might be worthwhile to quickly check that. Perhaps they have built this in to their embedded interpreter in the C-level. If so, then you could have a peek how they keep track of Out.

This seems curious - why use mode=“eval” and then exec()? Wouldn’t it be more normal to either use mode=“exec” (the default), or to subsequently eval() the result?

You’re right. This was sth I just quickly hacked out. It would indeed be simpler to use eval… My thought process was a bit convoluted…
So, you could do sth like:

Out = {}
def my_eval(s):
     key = f"_t{len(Out)}"
     try:
         Out[key] = eval(s)
     except SyntaxError:
         Out[key] = exec(s, globals())
      return Out[key]

Guys, this is far beyond my python knowledge.
But on the other hand I understand, that this is what I need. So i need to dive into it!

Here you find my current python eval function.
https://github.com/gsohler/openscad/blob/python/src/core/pyopenscad.cc at starting at line 486
Maybe i can implement this into my “init catcher code” …

Just for understanding the strategy: when my_eval gets a python program with several statements, functions, will this eval all at once, or once for each statement ?

Thank you for your valuable support!

Today I managed to achieve what I wanted.
Python has an internal token compiler and running python is executing these tokens.
Key to success was to

  • duplicate POP_TOP token to POP_TOP_CAPTURE token, then use it in
    compiler.c: compiler_stmt_expr
  • duplicated POP_TOP_CAPTURE token does in addition in bytecodes.c:
	    if(object_capture_callback != NULL) {
		    object_capture_callback(value);
	    }

Now i am happy with my solution. However i need to develop a patch to apply it with every new python version. doubt that a PEP will make its way in near future