Scripts ignore variables declared in 'module' scope

Say I have the following code:

# test.py
def foo():
    return 'foo1' + bar()

def bar():
    return 'bar1'

This works correctly in pure Python (tested on 2.7.1):

$ python
>>> import test
>>> test.foo()
'foo1bar1'

However, putting this into Ignition as a script module does not work:

from app import test
test.foo() # NameError: global name 'bar' is not defined

Some existing posts show similar behaviour, and I am confused by the responses:

  • “This has to do with how Python’s scoping rules work… You’re importing fpmi, but your def defines a new scope. The newly defined function doesn’t share scope with its declaring module.” (viewtopic.php?p=3984#p3984)
  • “This is just a quirk of Python scoping rules. Your logic is sound, but your init function doesn’t actually have “MyClass” in its scope” (viewtopic.php?p=9135#p9135)
  • “Python only ever has a local and global namespace” (viewtopic.php?p=7171#p7171)

Looking elsewhere in the ‘pure’ Python world (as opposed to Jython and Ignition) gives a very different impression. All the documentation I can find states that Python has a module level scope. If a variable does not exist within the local scope, it should then search up the hierarchy, including the ‘module’ (or ‘file’) that the function was defined in. My own experience with Python confirms this.

Looking back, it would appear that Python has had module scope since at least before 2.1:
docs.python.org/dev/whatsnew/2.1 … ted-scopes

It’s possible that this behaviour is caused by Jython, but it seems more likely that it’s down to the way Ignition’s Script Modules are loaded.

I would like to avoid having to add explicit imports to every function if at all possible.

I’m still somewhat unclear on how the script modules are loaded, which is making it hard to find a workaround. Any more information on this issue would be much appreciated.

Thanks.

To further clarify, here is an even simpler example:

module_var = 'hello'

def test():
    print module_var # NameError: global name 'module_var' is not defined

print module_var

The module_var is correctly printed when the module is first imported, but when test() runs later it does not search the module scope and so fails to find module_var. This example works correctly in ‘pure’ Python.

Most interestingly, saving the above example as /tmp/test.py and running the following in the script playground also works:

import sys
path = '/tmp'
if path not in sys.path:
	sys.path.append(path)
import test
test.test()

From what I know:

Regular python import is done as:

from module import function

then in the code write:

function()

OR

import module 

then in the script write:

module.function()

in the ignition software however you have modules in modules. So thinking about it in that way you are pretty much doing:

from module import module

when you do:

from app import test.

using the import line, not sure what you would do for a workaround, maybe from app.test import foo, but I know in the actual script writing the following should work.

print app.test.foo() 

When Ignition was upgraded to include the newer version of Jython there were a few things that changed and one of them happened to do with this scoping issue. You have to include app in each of your function definitions in order to call another function that was defined in the same module. So if you just add import app to all of your functions then everything should work as expected.

Mdunnick, that’s not the issue here. This is about variable scope. It only concerns imports as far as “from foo.bar import baz” creates a variable ‘baz’ (which is a ‘module’ object) in the current scope. The problem is that the module-level scope is not visible to the function like it should be.

Dave, I am assuming you mean that I have to use the full path all the time like this:

# test.py
function foo():
    import app
    app.test.bar()

function bar():
    print 'bar1'

This is something that I really want to avoid.

Do you have any links to more info on Jython’s part in this please? The results of my previous test imply that Jython is working correctly, and that the issue is specific to the way Script Module system works.

Thanks.

Unfortunately as of right now I don’t have any further information about this issue. I have made a ticket about this issue. For now you will have to import app and then call the function as you described above.

It’s been a while since I looked at the scripting code, particularly the script module system which has remained untouched since the beginning of time, but I suspect the root of the issue is that Ignition script modules aren’t real python modules, just a feature-incomplete facsimile that lets users organize their scripts a bit.

This also makes class inheritance very clumsy, as you have to import the full path to the superclass every time you want to do method chaining…

# app.foo.bar
class Parent(object):
    def __init__(self, name):
        pass

class Child(Parent):
    def __init__(self, name):
        import app
        super(app.foo.bar.Parent, self).__init__(name)
        # should just be:
        super(Parent, self).__init__(name)

With long names and paths this becomes unreadable very quickly, not to mention all the extra import lines in every single method :frowning:

I’ve created a bug ticket for this issue. Hopefully in the future you won’t have to do this anymore.

Notice that closures/inner functions work correctly:

def test():
	value = 'inner works'
	def inner():
		print value
	inner()

As soon as another level of scope is introduced everything works correctly, it’s just the outer module scope that doesn’t.

Incidentally, this is why all the examples for invokeLater pass in event=event, which is completely unnecessary…

# Currently this:
def requestFocus(event=event):
	import system
	system.gui.getParentWindow(event).getComponent('foo').requestFocusInWindow()
system.util.invokeLater(requestFocus)

# The following version works if you wrap the event in a function, but it's supposed to work without:
def handler(event):
	def requestFocus():
		system.gui.getParentWindow(event).getComponent('foo').requestFocusInWindow()
	system.util.invokeLater(requestFocus)
handler(event)

Any news on this?

Thanks.

There’s still a ticket for this but to be honest it’s really low priority.

I think systemparadox has a really good point here.

I verified that Ignition’s modules do not follow the scoping rules of Jython 2.5 modules.

Specifically, variables, classes and functions defined or imported in module scope cannot be seen in function definitions or method definitions without reimporting them. This is in contrast to how Python and Jython work.

I have personally gotten around this problem by making global variables, which is generally something I don’t want to do.

It would be really nice if Ignition modules worked like standard Jython modules not only to be consistent and compatible with Jython/Python but also because it makes writing classes and functions and using variables in modules simpler, easier and not awkward.

nmudge, would you mind posting a little bit more information about how you setup global variables to work around this? I tried briefly, but couldn’t get them to initialise consistently. While this doesn’t fix all of the issues, it would at least save having to import app and system everywhere.

I appreciate that fixing this could be fairly involved, but I really do think this needs to be taken a bit more seriously. It’s a pretty fundamental issue that causes confusion for even the most basic scripts and becomes progressively worse from there.

Thanks.

Sure. At the top of every Ignition script module I put these as the first two lines:

global app,system
import app,system

This allows me to use app and system everywhere in a module without having to import them again. This has always worked well for me.

It is possible to declare more global variables and then import or define more things that become global. Most often I only declare system and app global because everybody knows that global variables suck.

I asked Carl, one of the Directors of Software Engineering at Inductive Automation if adding Jython/Python module scope to Ignition script modules is something that I could help with.

He said yes and gave me direction on getting started with it. I plan to start working on it soon on a part-time basis.

I work for Inductive Automation Design Services.

Thanks nmudge.

As far as I can tell so far, it shouldn’t be too difficult a change to make. I just think it’s passing the wrong objects to globals() and locals() and/or not saving them correctly.

Ah that makes more sense now. Global imports work well for app and system. The issue I had was to do with circular dependencies in our helper package, but that should work too if I can resolve that. The side-effect of having to import inside every function is that it makes everything lazy-loaded by default.

Thanks.

We hope to address this in 7.7

What we’ll probably do is start phasing out the existing “app” scripts in favor of a new style of script modules that will have correct scoping and whose scripts will be individually locked. We also hope to introduce a “shared” script environment that would be global across all projects.

By “scripts will be individually locked” Carl means that multiple people will be able to edit different modules in the Script Module Editor at the same time.

Currently the entire Script Module Editor is locked so that only a single person can use it for editing at a time.

This doesn’t have to do with module scoping but is just additional information about plans for jython modules.