Is there a way to remove the sikuli import feature in the nightly build?

Asked by Ryan

I downloaded the nightly build to check some of the fixes out. After getting it configured I am running into the exception: ImportError: Login.sikuli has no Login.py. I believe it is because our project is not structured using the *.sikuli format. Is there a way to disable this in the importer? This has been working in the previous version for me.

With the ability to use Sikuli in an IDE it appears there is now a conflicting requirement. Python projects should be structured using packages and an __init__.py folder. When Eclipse is chosen as an IDE the *.sikuli syntax causes file structure issues. Upon refresh (or running the script maybe, I forget which) the "folder.sikuli" becomes a package structure where "folder" is a package and "sikuli" is a sub-package. This causes all kinds of problems and none of the files can be found anymore. Also, when using multiple .py files I found the need to add every .sikuli folder to the pythonpath (and/or in Eclipse itself - it was causing all kinds of problems) and the solution was to structure the project in a series of packages like Java.

All of the image files are accessible via the following:

curDir = os.path.dirname(os.path.realpath(__file__))
image = os.path.join(curDir, "image.png")

This also allows subclasses and other code to override the image file or curDir to re-use scripts.

Question information

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

to make it simple:

you have something like this in your script:
import Login

... and the script to be imported is in some Login.py found on sys.path.

Further I guess your main script is stored in some myscript.py and you are running it using plain Jython or from the Python plugin in Eclipse.

If this is true, then you might have in your main script
from org.sikuli.basics import SikulixForJython
from sikuli import *

as well as in the imported scripts, that use Sikulix features
from sikuli import *

please confirm or tell me, what you have instead.

... and no, the import feature cannot be switched off (but might be a good idea to be able to do that)

But anyways: Login.py should be imported without any problems. So if it is not imported, this is bug, I have to fix.

Revision history for this message
Ryan (ryan-pisano) said :
#2

I'll do my best to provide what I have but some of it I cannot disclose unfortunately.

The import part looks like this:

The entry point to Jython is a script entrypoint.py within entrypoint.sikuli. This script accepts a variable amount of arguments from the commandline when running. From here we are using reflection to determine what class/function to run based on the arguments.

The class to be run is imported as so: mod = __import__("%s.Components.%s" % (self.project, moduleName), fromlist=[moduleName]).
Essentially I am building "project.Component.Path.To.Module" and importing the module from it.

In my case this successfully begins importing Navigation.py. The failing line from this script is from ..Frames.Pages.Login import Login

Sikuli is introduced at the top level package. In this case it is Central/__init__.py. Central.py imports our sikuli wrapper VertexSikuli.py. This is simply to set all configuration and provide a very minimalist sikuli API access to reduce confusion. Sikuli is imported in this file as import sikuli.Sikuli as _sik.

I know you promote the use of global imports but we have designed our framework so that all objects either access sikuli functionality through their region or through super-class functionality using the contained region methods.

Hope this is helpful!

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

ok, thanks for the information.

I will introduce the possibility to switch off the SikuliX import feature. Watch the fixes with the nightly builds.

Until then, you might simply comment out the line (should be 78)

import SikuliImporter

in the file
Lib/sikuli/Sikuli.py

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

BTW: what I do not understand:
Why do you use .sikuli at all?
Why don't you use only .py files?

In your case you only need sikulixapi.jar on java class path and and the Lib/sikuli folder on sys.path (which would be done by 'import org.sikuli.script.SikulixForJython', see https://github.com/RaiMan/SikuliX-2014/wiki/Usage-in-Java-programming).

... and then use Jython to run your stuff from command line.

Revision history for this message
Ryan (ryan-pisano) said :
#5

Thanks!!

We are using the single .sikuli folder only as the entry point. It is called via 'java -jar sikuli-script.jar -r c:\path\to\executor.sikuli --args [arguments]'. It seemed like the easiest hands-off way to do it. There are several projects running with shared packages and test tools, so the executor.sikuli path is programatically changed based on what project is being run. In some projects there are images with this file and in some there are not but since it is being called from the command line we wanted to make sure it wasn't included in the project package (or even on jythonpath since it should never be called by another Jython script).

Are you saying I could simply begin a Jython session and call the .py instead of going through the sikuli jar file?

The import is working now, but when I attempt to find the image via absolute path it is returning

FindFailed: null
  Line 2520, in file Region.java

I tried making it a pattern as well. When printing the variable I do see the path to the proper image, however.

Revision history for this message
Ryan (ryan-pisano) said :
#6

It looks like there is something more to this so I will provide some more detail:

An object is looking to find a series of buttons to assign regions to. I am first assigning the image to a dictionary like this:

buttons = {"Login": self.loginButton} # self.loginButton is a string path to the image file.

These buttons are then iterated through to create regions in another object like so:
for button in buttons:
    img = buttons[button]
    buttons[button] = Button(self.find(img)) # The object is a subclass of Region so self.find() is calling the find() on a region.

Is this a new limitation to subclassing the region objects not found in the previous release?

Revision history for this message
Ryan (ryan-pisano) said :
#7

I don't know how to edit the question above so I'm going to add an additional comment:

It does appear that it is directly related to subclassing region. We were only subclassing to access the basic functions (click, left, right, getX...) not to override them (Which was the issue in the previous release). I have a simple solution to fix this in my case, but hopefully anybody searching for the answer will come across this and note that subclassing at all doesn't appear to work in 1.1.0. It was already stated in another answer that subclassing Region isn't supported.

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

I want to be sure, that we are still talking about the latest build of 1.1.0, since you say you are using:

'java -jar sikuli-script.jar -r c:\path\to\executor.sikuli --args [arguments]'

... this does not work with 1.1.0, because there is no sikuli-script.jar in 1.1.0.

Furthermore the exception (comment #5) does not correlate with the Region.java of 1.1.0, so I doubt, that you are really using 1.1.0

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

I just made some evaluation with 1.1.0.

Subclassing of Region on the Python scripting level is only possible if you either
1. do not use __init__ in your subclass
or
2. if you use __init__, you have to take care, that your Region object has valid pos (x,y) and dim (w,h)

--- in case 1.
the standard constructors are available (which are implemented on the Java level only). If you need initialisation of your subclassed Region objects, you have to find another way for that, for example

reg = subRegion(<region init parameters>)
reg.init(<your private parameters>)

--- in case 2:
you have to implement an __init__ that accepts the standard Region init parameters (x,y,w,h or another Region object) and then use
self.setROI(<region init parameters>)

to make your Region valid.

this is an example:

class XRegion(Region):

  def __init__(self, *args):
    if len(args) == 1:
      self.setROI(args[0])
    elif len(args) == 4:
      self.setROI(args[0], args[1], args[2], args[3])
    else:
      self.setROI(SCREEN)
    self.img = "image.png"

  def doFind(self):
    m = self.find(self.img).highlight(2)
    return m

print XRegion()
reg = XRegion(0,0,500,500)
print reg
reg = XRegion(reg)
print reg
reg = XRegion(reg.doFind())
print reg

prints:
R[0,0 1440x900]@S(0)[0,0 1440x900] E:Y, T:3.0
R[0,0 500x500]@S(0)[0,0 1440x900] E:Y, T:3.0
R[0,0 500x500]@S(0)[0,0 1440x900] E:Y, T:3.0
R[257,119 58x72]@S(0)[0,0 1440x900] E:Y, T:3.0

to add userargs to the constructor, you have to implement an __init__ like this:
  def __init__(self, *args, **userargs):

and handle the userargs keyword parameters accordingly

another maybe easier implementation is to use

  def __init__(self, *args):

and always use the first parameter as <region init parameter> as a list, that either contains 4 values or 1 value or is empty. Of course you have to adapt the handling in __init__ accordingly.

usage:
XRegion([], userparm1, userparm2)
reg = XRegion((0,0,500,500), userparm1, userparm2)
XRegion((reg,), userparm1, userparm2)

Revision history for this message
Ryan (ryan-pisano) said :
#10

Yes we are still talking 1.1.0 with the issue, sorry. I was describing the existing way that Sikuli is launched in reference to the question about using the .sikuli folder. In the case of finally switching to 1.1.0 that line would be changed, however I was only running my testing through eclipse at this point.

I'm not sure why the traceback wouldn't correlate to 1.1.0 but I made sure to remove all of the existing sikuli data and re-install clean. I actually updated to the final release of Jython at the same time, so I know the jythonpath was cleared of old modules as well.
In that example I attempted to find the image before passing it off to the other object and it was successful, and printing the image/pattern within the object that is supposed to look for it showed the filepath. I was even able to use the region itself within that object, but only when it was self.find(img) did it return that error.

What I have been doing is operating under the assumption that, except in special cases, an object will not be called without knowing it's region on the screen. For example, after creating a region containing a text box based some find operation, that region is passed into TextBox:

class TextBox(SomeSuperClass):
    def __init__(region, name=TextBox):
        """@type region: Region"""
        super(TextBox, self).__init__(region, name)
        # Anything else the Text Box needs

    def severalMethods():
        do stuff

region is assumed to be a Region in some capacity and even if it is one of my existing objects, type-hinting in the docstring helps assume it is some kind of Region. Eventually there is a super class that instantiates the region:

class BaseClass(Region):
    def __init__(region, name, sikuli=sikuli, lookup=None, curDir=None):
        super(BaseClass, self).__init__(region)
        self.region=region # Actually wrapped in a property with a depreciation warning to keep backwards compatibility between our frameworks.

    def methodsIHaveAbstracted():
        do stuff

In the specific case of my issue, the constructor received it's own Region as region (and calls the init of the super providing this region) and then attempts to use the find method found through inheritance to search for this button. This is where I try self.region.find(img) or even just region.find(img) (should be the same anyway) and am able to find the region.

Are you saying that instead of the BaseClass using super().__init__(region) it should be using Region.init(region)?

While reviewing what I wrote above I tried some things. I printed out self.getX() (and y, w, h) and received 0, 0 900, 1600 as expected (the region is the entire screen). I then tried to do self.highlight(3) on the next line and logged the following:
2015-08-20 08:49:04,789 DEBUG - __init__:60 - (<type 'java.lang.NullPointerException'>, java.lang.NullPointerException, <traceback object at 0x2>)

Sorry to keep at it, I'm just going to need to prepare a description of what is and is not possible in the new version to my team so I want to make sure I understand the changes and limitations. My goal was run exactly what I have in place in 1.1.0 and see what breaks and then go from there.

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

Thanks for staying with this issue - very helpful.

I have to admit, that I was not aware of the problem.
Up to 1.0.1 there indeed was an __init__ in class Region at the Python level.

With version 1.1.0+ the class Region at the Python level is only still there, to stay backwards compatible on 'with someRegion:' and the support for the undotted usage of Region methods (which then are bound automatically to the primary screen).

So to keep things simple for you and others, I am trying to understand your approach.
I am testing with the latest 1.1.0 using Jython 2.7

actual problem:

testing with

class BaseClass(Region):
    def __init__(region, name, sikuli=sikuli, lookup=None, curDir=None):
        super(BaseClass, self).__init__(region)

I get the error:
[error] NameError ( global name 'self' is not defined )

which based on my Python understanding is inevitably

So what is your trick to have this working?

-- Are you saying that instead of the BaseClass using super().__init__(region) it should be using Region.init(region)?
this might be a possible solution, but a Region.init() does not exists yet.

Revision history for this message
Ryan (ryan-pisano) said :
#12

Oh my gosh, I'm sorry... I forgot to put in the self parameters for the def statements. I have Eclipse set up to do that for me so I haven't typed self as a parameter in a really long time.

class BaseClass(Region):
    def __init__(self, region, name):
        super(BaseClass, self).__init__(region)

reg = sikuli.Region(sikuli.screen())
baseClass = BaseClass(reg)

It would look like this. you can omit sikuli (that is just passing the sikuli wrapper we created with helper functions), and the lookup (secret :) ). curDir is just used to provide a working directory to any images that may be called in any of the class levels. I just included them for the context of more being included at that level, but they can just be omitted for a simplest working example. In the example I also included the way I am getting the screen's Region for completeness.

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

ok, I think I have a solution for use cases like yours:

I have added an init method to the Java level:

public void init(Region r) {
    if (!r.isValid()) {
      return;
    }
    x = r.x;
    y = r.y;
    w = r.w;
    h = r.h;
    scr = r.getScreen();
    otherScreen =r.isOtherScreen();
    rows = 0;
    autoWaitTimeout = r.autoWaitTimeout;
    findFailedResponse = r.findFailedResponse;
    throwException = r.throwException;
    waitScanRate = r.waitScanRate;
    observeScanRate = r.observeScanRate;
    repeatWaitTime = r.repeatWaitTime;
}

- it does nothing if the Region object is not valid (has no screen or width or height is 0) (in the consequence, the Region is not valid and might produce errors/exceptions).
- with a valid Region object it copies the basic attributes from the given Region object

so in your case you have to say:

class BaseClass(Region):
    def __init__(self, region, name):
        self.init(region)

reg = sikuli.Region(sikuli.screen())
baseClass = BaseClass(reg)

so you only in your BaseClass you have to replace the super().__init__() call with the call of init()

This works with tomorrows build.
I appreciate much, if you thoroughly test it.

Revision history for this message
Ryan (ryan-pisano) said :
#14

Thanks! That is very helpful!

I'll do my best to test it. Currently every object in my framework will use this init method with some kind of derivative of a Region... sometimes a Region, sometimes a Match, sometimes my own objects. I'm not sure what else would go into testing it as I don't know much about the Jython/Java layer specifics.

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

As far as I understand:
The reference to the SikuliX super Class Region is only in one place: something you called BaseClass above.

If this is true, then only there you have to make the mentioned change.

All classes derived from that BaseClass are ok, because the BaseClass has a valid __init__() now.

Revision history for this message
Ryan (ryan-pisano) said :
#16

Yes that is correct. Sorry, I was referring to your comment if I could thoroughly test it. I was just saying I'm not sure what to even test beyond what I am currently doing.

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

ok, then just with tomorrows build make this change and check wether it works.

Revision history for this message
Ryan (ryan-pisano) said :
#18

This is working for me now!