IronLanguages / ironpython2

Implementation of the Python programming language for .NET Framework; built on top of the Dynamic Language Runtime (DLR).
http://ironpython.net
Apache License 2.0
1.07k stars 229 forks source link

Calling a previous called library throws UnboundNameException #822

Closed ChrisAcrobat closed 2 years ago

ChrisAcrobat commented 2 years ago

Prerequisites

The issue tracker is used to report bugs and request new features, NOT to ask questions.

Questions should be posted to the users mailing list which can be accessed at https://ironpython.groups.io/g/users.

Description

We get IronPython.Runtime.UnboundNameException when we call a previous called library in a script. LibA -> LibB -> LibC -> LibA -> UnboundNameException Our python host implementation is base on this Stack Overflow response: https://stackoverflow.com/a/4644585 Un-commenting the # Workaround rows and import the lib before it is called fixes the issue, but does not scale very well.

Steps to Reproduce

# Test script
from LibStaticA import *
from LibInstanceA import *
testCalls = 10

print "Static test"
try:
    LibStaticA().Test(testCalls)
except Exception as ex:
    print ex.ToString().Split("\n")[0]

print "Instance test"
try:
    LibInstanceA().Test(testCalls)
except Exception as ex:
    print ex.ToString().Split("\n")[0]
# LibInstanceA
from LibInstanceB import *

class LibInstanceA:
    def Test(self, num):
    #   from LibInstanceA import * # Workaround
        LInstanceB().Test(num)
# LibInstanceB
from LibInstanceC import *

class LInstanceB:
    def Test(self, num):
    #   from LibInstanceC import * # Workaround
        LibInstanceC().Test(num)
# LibInstanceC
from LibInstanceA import *

class LibInstanceC:
    def Test(self, num):
        if 0 < num:
            print "LibInstanceC laps remaining: "+unicode(num)
        #   from LibInstanceA import * # Workaround
            LibInstanceA().Test(num-1)
        else:
            print "Done!"
# LibStaticA
from LibStaticB import *

class LibStaticA:
    @staticmethod
    def Test(num):
    #   from LibStaticB import * # Workaround
        LibStaticB.Test(num)
# LibStaticB
from LibStaticC import *

class LibStaticB:
    @staticmethod
    def Test(num):
    #   from LibStaticC import * # Workaround
        LibStaticC.Test(num)
# LibStaticC
from LibStaticA import *

class LibStaticC:
    @staticmethod
    def Test(num):
        if 0 < num:
            print "LibStaticC laps remaining: "+unicode(num)
        #   from LibStaticA import * # Workaround
            LibStaticA.Test(num-1)
        else:
            print "Done!"

Expected behavior: [What you expected to happen]

Static test
LibStaticC laps remaining: 10
LibStaticC laps remaining: 9
LibStaticC laps remaining: 8
LibStaticC laps remaining: 7
LibStaticC laps remaining: 6
LibStaticC laps remaining: 5
LibStaticC laps remaining: 4
LibStaticC laps remaining: 3
LibStaticC laps remaining: 2
LibStaticC laps remaining: 1
Done!
Instance test
LibInstanceC laps remaining: 10
LibInstanceC laps remaining: 9
LibInstanceC laps remaining: 8
LibInstanceC laps remaining: 7
LibInstanceC laps remaining: 6
LibInstanceC laps remaining: 5
LibInstanceC laps remaining: 4
LibInstanceC laps remaining: 3
LibInstanceC laps remaining: 2
LibInstanceC laps remaining: 1
Done!

Actual behavior: [What actually happened]

Static test
LibStaticC laps remaining: 10
IronPython.Runtime.UnboundNameException: global name 'LibStaticA' is not defined
Instance test
LibInstanceC laps remaining: 10
IronPython.Runtime.UnboundNameException: global name 'LibInstanceA' is not defined

Any idea on what could be the source of our problem? Is there any other information that I could try to provide that would help?

Versions

You can get this information from executing ipy -V.

Unable to execute, but has been tested on 2.7.12.

slozier commented 2 years ago

I believe the NameError (which shows up as UnboundNameException in C#) is the expected behavior. CPython results in exactly the same error.

Calling from LibStaticA import * will import what's in LibStaticA at the time of the call and not what would eventually be loaded. For example, when you run your main file, as soon as it hits from LibStaticA import * it executes LibStaticA which in turn loads LibStaticB and then LibStaticC, but when you do the from LibStaticA import * at the top of LibStaticC, there's nothing loaded in the LibStaticA module.

You can easily check this if you edit the top of LibStaticA:

# LibStaticA
A = 1
from LibStaticB import *
B = 2

You can use A in LibStaticC, but you will get an exception if you try to use B.

This type of circular dependency A->B->C->A is probably not recommended, but if it's really what you need then the only thing that comes to mind would be to not use from LibStaticA import * and go with something like import LibStaticA and later call it as LibStaticA.LibStaticA.Test(num-1).

ChrisAcrobat commented 2 years ago

Thanks for your suggestion, I'll respond again after we have tested it.

ChrisAcrobat commented 2 years ago

Added the following test:

# The test
from LibDirectStaticAFile import *
testCalls = 10

print "Direct test"
try:
    LibDirectStaticAFile.LibDirectStaticA.Test(testCalls)
except Exception as ex:
    print ex.ToString().Split("\n")[0]
# LibDirectStaticAFile
from LibDirectStaticBFile import *

class LibDirectStaticA:
    @staticmethod
    def Test(num):
        LibDirectStaticBFile.LibDirectStaticB.Test(num)
# LibDirectStaticBFile
from LibDirectStaticCFile import *

class LibDirectStaticB:
    @staticmethod
    def Test(num):
        LibDirectStaticCFile.LibDirectStaticC.Test(num)
# LibDirectStaticCFile
from LibDirectStaticAFile import *

class LibDirectStaticC:
    @staticmethod
    def Test(num):
        if 0 < num:
            print "LibDirectStaticC laps remaining: "+unicode(num)
            LibDirectStaticA.LibDirectStaticA.Test(num-1)
        else:
            print "Done!"

Got the following result:

Direct test
IronPython.Runtime.UnboundNameException: name 'LibDirectStaticAFile' is not defined

Note that it only says name and not global name as before. Could that be an error with our implementation?

PS: I have also renamed the other lib sources to *File. I mention it now in case reference them in the future but forget to mention it then.

slozier commented 2 years ago

Should be more like (difference is in the import):

# LibDirectStaticCFile
import LibDirectStaticCFile

class LibDirectStaticC:
    @staticmethod
    def Test(num):
        if 0 < num:
            print "LibDirectStaticC laps remaining: "+unicode(num)
            LibDirectStaticCFile.LibDirectStaticA.Test(num-1)
        else:
            print "Done!"
ChrisAcrobat commented 2 years ago

Ops, fixed that and now it works! Thank you for you help! 🥇