Sikuli code organization / code reuse

Asked by David

I discovered a solution for the below problem. My current solution however isn't quite ideal, and I'd still love any feedback. Here's the solution:

I created a directory structure as follows:

parent.sikuli
parent.sikuli/project1.sikuli/
parent.sikuli/project1.sikuli/(sikuli objects go here)
parent.sikuli/shared.sikuli/
parent.sikuli/shared.sikuli/(sikuli objects go here)

Each parent .sikuli directory is essentially an empty file that you can use to initialize other imports. For example:

parent.sikuli/parent.py
------------------------------
from project1.sikuli import *
from shared.sikuli import *

shared.sikuli/shared.py
------------------------------
from ReusableComponent1 import *
from ReusableComponent2 import *

Still, this has the problem that you need to change an initialization file each time you add a new object to the directory structure. It would be nice if there was a way to recursively import everything. Also, it's a little hacky and doesn't allow you to selectively import certain files. Furthermore, this creates a bit of dependency hell, since components at the project level can't directly import shared components.

------------------------------------------------------------------------------------------------------
Question:
I am trying to create a good structure for Sikuli code to use across many tests on many projects. I am trying to organize my code as follows:

parent/
parent/project1
parent/project1/(project1 .sikuli files here)
parent/shared
parent/shared/(shared .sikuli files here)

In order to do this, there needs to be a way to either:
1) Package the project at the parent level
2) Import the other modules dynamically

I couldn't find a way to create a sikuli package, so I went with method #2, which is fine. The problem with method #2 is that you need to import all of the folders dynamically into os.path for _every_ file. This is really a big problem. Also, I'm currently statically getting the directories by using getBundlePath(). This is another problem because you have to update this anytime you add another folder to the directory structure.

In other words, In order to import other modules dynamically, I need to include something similar to the following code at the top of _each_ .py file.

import os
project1Path = os.path.dirname(getBundlePath()) + "\.." + "\project1Path"
if not project1Path in sys.path: sys.path.append(project1Path)
sharedPath = os.path.dirname(getBundlePath()) + "\.." + "\shared"
if not sharedPath in sys.path: sys.path.append(sharedPath)

Is there a way that I can:
1) Create one __init__.py file (or similar) to preload all of the modules required so that I do not have to repeat the previous code at the top of _every_ file?
2) Recursively get all of the folders under the parent directory and import all the .sikuli modules so I do not have to update the __init__ file every time there is a change to the directory structure?

Alternatively, is there a way to create a package of .sikuli files?

The only solution I can get to work at the moment is to actually throw all the .sikuli files into one parent folder and then use a naming convention as follows:

parent/
parent/Project1Object1.sikuli
parent/Project1Object2.sikuli
parent/Project1Object3.sikuli
parent/SharedComponent1.sikuli
etc...

Then you can use the following code at the top of each file:

import os
myPath = os.path.dirname(getBundlePath())
if not myPath in sys.path: sys.path.append(myPath)

This code shouldn't change if you keep a flat directory structure. Even so, this isn't very ideal because you're still copy-paste coding this snippet into the top of every file instead of maintaining it in one place, and if you're using thousands of .sikuli scripts (or more) then things will get messy.

Question information

Language:
English Edit question
Status:
Solved
For:
SikuliX Edit question
Assignee:
No assignee Edit question
Solved by:
David
Solved:
Last query:
Last reply:
Revision history for this message
RaiMan (raimund-hocke) said :
#1

--1. do not use folder names .sikuli ...
... if they do not contain a Sikuli script and its resources
Because this collides with the internal import support for .sikuli , which will import the contained .py and add the folder .sikuli to the image path automagically by only using (for a sub.sikuli)
import sub

--2. in a flat organization ...
... where all .sikuli files are in one folder, there is no need for sys.path manipulation.
just saying
import sub
will find the sub.sikuli in the same folder as the importing .sikuli resides.

--3. in a rather fixed substructure ...
you can use one support script, that you import using
from support import *
and delegate the rest to the script support.sikuli
(this is why from sikuli import * reveals all Sikuli features)

--4. in a living structure ...
... use a naming convention for the scripts and scan the subtree in support.sikuli and use
exec"import "+evaluated_script_name
then
from support import *
can still be used

--5. location for bootstrap scripts
If you have those generally supporting scripts, that you want to import all around the place, put them into a folder named "Lib" (you might have to create it), in the same folder as sikuli-script.jar resides.
This folder is in sys.path per default (a Jython convention).

Revision history for this message
j (j-the-k) said :
#2

If you don't store your images in the submodules you could get rid of the .sikuli directories and store everything in a python module structure like this:

parent/
parent/module1/
parent/module1/__init__.py
parent/module1/submodule1.py
parent/module1/submodule2/__init__.py

Then you only need to add
sys.path.append(path/to/parent)
because python scans the underlying package structure itself.

The problem ist, you then need to explicitly specify your image locations because sikuli will not search in those subdirectories and you have to add the "from sikuli import*" to every python file to use the Sikuli features, but everything else should work.

Revision history for this message
David (dhechols) said :
#3

--2. in a flat organization ...
... where all .sikuli files are in one folder, there is no need for sys.path manipulation.
just saying
import sub
will find the sub.sikuli in the same folder as the importing .sikuli resides.

I've tried this method in the past and I always end up with the error: TypeError: 'module' object is not callable

For example, if I do the following I will get the error:

import sub
class usesSub:
          def s1(self):
               s = Sub()
               s.doSomething()

Revision history for this message
RaiMan (raimund-hocke) said :
#4

the correct usage is:

import sub
class usesSub:
          def s1(self):
               sub.doSomething()

or
from sub import *
class usesSub:
          def s1(self):
               doSomething()

In the second case all names from sub are taken in the global name space which might lead to name clashes if there is no strict naming convention.

So the first case is more robust with a little bit more to type.

Revision history for this message
David (dhechols) said :
#5

RaiMan, I'm sorry to say, but this just isn't working. Furthermore, your example doesn't make much sense because you're calling the object's attribute, and not the method.

So let's say we have a class sub:

class sub:
         i = "12345"
         def doSomething(self):
                  pass
         def doSomethingElse(self):
                  pass
         def doAnotherThing(self):
                  pass

I'm sure you know this, but I want to make sure we're on the same page here. In other words you could say:

sub().doSomething()

and this would dynamically create an object instance in Python that would be created just for executing this method.

Usually though, it isn't efficient to do this because it will assign a new object every time. An example of this would be:

sub().doSomething()
sub().doSomethingElse()
sub().doAnotherThing()

Instead you usually would create an object instance and use that so you don't have to create a new object every time:

s = sub()
s.doSomething()
s.doSomethingElse()
s.doAnotherThing()

So let's go back to your example:

import sub
class test:
          def s1(self):
               sub.doSomething()

The problem with this is that sub.doSomething() returns sub's attribute "doSomething", which doesn't exist. If you ran this code you would get the error:

AttributeError: 'module' object has no attribute 'doSomething'

Instead, if we changed the code to the following, it would return the attribute i as the string "12345":

import sub
class test:
          def s1(self):
               return sub.i

But this still doesn't let me instantiate the object properly. Not only that, but I'm running into a whole bunch of problems with this approach, which I've outlined below.

--------------------------------------------------------------------------------------------------------------

When I do:

import sub

I get the error:
"No module named sub"

Even though I have sub.sikuli in the same directory as test.sikuli

In other words, my directory structure is:

/test/
/test/sub.sikuli
/test/test.sikuli

If I explicitly get the path, it resolves this problem:

import os
myPath = os.path.dirname(getBundlePath())
if not myPath in sys.path: sys.path.append(myPath)

but then I still can't properly instantiate an object

import sub
class test:
 def t1(self):
  s = sub() #TypeError: 'module' object not callable
  s.s1()

I'm using Sikuli X-1.0rc3(r905)

I wish what you said above worked. This is making me sad. :( I tried to include as many details as I possibly could to try to resolve the issue.

Revision history for this message
David (dhechols) said :
#6

A quick edit to the last post. The final code snippet should be:

import sub
class test:
 def t1(self):
  s = sub() #TypeError: 'module' object not callable
  s.doSomething()

Revision history for this message
David (dhechols) said :
#7

I sat down this evening and did a fresh install of Sikuli X-1.0rc3(r905), Python 3.3, and Java 6.0.310 (x86). I tested out various methods of importing. Here's my results. This suggests that the only way to do a correct import is to use the "from module import *" method, and that other methods fail. Furthermore, this does NOT create namespace conflicts.

Project Structure:
C:\Users\User\Documents\
C:\Users\User\Documents\sub.sikuli
C:\Users\User\Documents\test.sikuli

sub.sikuli
--------
from sikuli import *
class sub:
    def doSomething(self):
        popup("it worked")

Test #1: Can we import from the same directory with a straight import?
Result #1: Yes, but this does not allow us to instantiate Python objects correctly.

test.sikuli
-------
import sub

if __name__ == '__main__':
    sub().doSomething() #TypeError: 'module' object is not callable

Test #2: Can we import from the same directory using from?
Result #2: Yes, and it works.

test.sikuli
-------
from sub import *

if __name__ == '__main__':
    sub().doSomething() #It works

Test #3: Can we import using from and call doSomething() directly?
Result #3: No.

test.sikuli
-------
import sub

if __name__ == '__main__':
    doSomething() #NameError: name 'doSomething' is not defined

Test #4: Will importing multiple modules cause namespace conflicts if we have multiple doSomething() methods?
Result #4: No namespace conflicts will occur. The local call will always prefer either the local module. If the method does not exist there, it will use the last imported module's method. (This is expected Python behavior.)

sub.sikuli
-------
from sikuli import *

def doSomething():
     popup("this is sub")

class sub:
    def doSomething(self):
        popup("this is sub class")

hub.sikuli
-------
from sikuli import *

def doSomething():
     popup("this is hub")

class hub:
    def doSomething(self):
        popup("this is hub class")

test.sikuli
-------
import sub
import hub

def doSomething():
     popup("this is test")

class test:
    def doSomething(self):
        popup("this is test class")

if __name__ == '__main__':
    sub().doSomething() #this is sub class
    hub().doSomething() #this is hub class
    sub().doSomething() #this is sub class
    s = sub()
    h = hub()
    s.doSomething() #this is sub class
    h.doSomething() #this is hub class
    t = test()
    t.doSomething() #this is test class
    doSomething() #this is test
    #if test.sikuli did not have doSomething()
    doSomething() #this is hub

This suggests a few things:
#1: Sikuli's automatic import using the form "import module" is broken because it does not allow you to use Python objects correctly.
#2: Using the form "from module import *" works just fine, without namespace conflicts. Namespaces resolve as expected in Python.
#3: There only good way to create a sikuli project is a flat structure. It might make sense to use Python's package management instead as J suggested. I've yet to experiment with this, but it sounds promising because it side-steps Sikuli's odd importing.
--------------------------------------------
If you don't store your images in the submodules you could get rid of the .sikuli directories and store everything in a python module structure like this:

parent/
parent/module1/
parent/module1/__init__.py
parent/module1/submodule1.py
parent/module1/submodule2/__init__.py

Then you only need to add
sys.path.append(path/to/parent)
because python scans the underlying package structure itself.

The problem ist, you then need to explicitly specify your image locations because sikuli will not search in those subdirectories and you have to add the "from sikuli import*" to every python file to use the Sikuli features, but everything else should work.

Revision history for this message
RaiMan (raimund-hocke) said :
#8

--- Python
...does not matter for Sikuli. it uses the bundled Jython 2.5.2, which is a Java implementation of Python on language level 2.5

--- Sikuli's automatic import using the form "import module" is broken
... is not true. It just uses the Python original import feature after intervening the fact, that the .py is found in a .sikuli when you say
import something

and something.sikuli is found in one of the standard places (mainly same directory and sys.path)

--- your example giving the name error

sub.sikuli
--------
from sikuli import *
class sub:
    def doSomething(self):
        popup("it worked")

test.sikuli
-------
import sub
sub().doSomething() #TypeError: 'module' object is not callable

this gives an error in C-Python too, because you mean the class name sub, which is not known in the current namespace (would only be, if you would use <from sub import *>.

correct usage in this case:
sub.sub().doSomething()

**** your conclusion is completely wrong
... except one thing: something like "Sikuli packages" is not easily possible like with the Python __init__()

your #1: wrong see above

your #2: namespace conflicts mean, that if the same name exists in the current namespace and the namespace of the imported module, this name will only be reachable in this case using sub.name, because name alone will reference the object in the current namespace (each module in Python has its own namespace, plus the differentiation inside a module in global and local (inside classes and refs).

your #3: nearly wrong: currently you have to put all folders in the structure containing .sikuli folders to be imported into sys.path, since Sikuli not yet supports package structure (but I put it on the list)

Sikuli's import feature was mainly implemented, to allow each module to have its own image store (but an image filename should be unique in a structure of imports, since the first image top down sys.path is taken)

So besides the missing package feature everything with importing .sikuli works as expected.

BTW: the construct
if __name__ == '__main__':
is only needed in a Python module if it can be used as an imported module or as the top level module given to the Python interpreter.
IMHO in Sikuli scripts this is not needed.
One situation:
if you implement tests for the sub module features inside the submodule, this can be elegantly masked this way, so the test are run only, if the submodule is run as a main.

Revision history for this message
David (dhechols) said :
#9

>>>this gives an error in C-Python too, because you mean the class name sub, which is not known in the current namespace (would only be, if you would use <from sub import *>.

>>> correct usage in this case:
>>> sub.sub().doSomething()

Ah, my mistake. Doh. I forgot that when you want to instantiate an object you need to reference the module. IE:

s = Sub() #this is trying to instantiate an object from a class called Sub in the current module
s = sub.Sub() #this is instantiating an object from a class called Sub in the sub module.

So that resolves the import problem.

Thank you for all your help. I learned lots. <3 :D