Best practice for "global object registry" in Jython?

What is currently considered to be the best practice when it comes to having a “global” object (not: Item) registry in Jython, that can be shared among scripts? (apart from not having any, that is!)

I’d like to replace virtual Items by something more flexible.

First of all, a global Object may or may not be the best way to do this. Have you looked at Item Metadata? Design Pattern: Using Item Metadata as an Alternative to Several DPs

I believe the best practice is to use configuration.py or create a module. Instantiate your Object when the module get’s loaded and import it into each of your scripts that care about it. I imagine you could implement some sort of Singleton DP if necessary.

But in general, I think having a global blob that you store a bunch of stuff into would be an anti-pattern. I suspect there might be more appropriate approaches.

1 Like

Thanks! I’ve looked into metadata, and I can see how it is helpful to add more information to existing items.

What I’m looking for is a bit different, and not really an implementation option for a Singleton DP (even less, of course, for a Blob anti-pattern), but rather more like an implementation option for the Blackboard DP - a central location to place shared information on. Similiar like “ir” for Python objects, if you will.

My current solution is indeed a module.

I mainly use dicts in modules for storing/sharing data between scripts and rules, although sometimes it makes sense to add them as attributes of a function, module, etc. If the data needs to be persisted, then I use metadata. A third option which is not very well known is to use ScriptExtensions. These are a powerful tool that we have in scripted automation and will play a large part in the new Scripting API. Basically, you can create your own ScriptExtension presets, which can store objects that can be shared between scripts, even if they are in different scripting languages. These are what is used to add objects to the default script scope. 200_JythonExtensionProvider.py is a component script that creates core.JythonExtensionProvider. Use this to create/modify your custom presets. Here is a script to illustrate the usage that I will be adding to the documentation…

"""
This is an example script to illustrate the creation/modification of
ScriptExtension presets. Save the script file twice.
"""
from core.log import logging, LOG_PREFIX#, log_traceback
LOG = logging.getLogger("{}.TEST".format(LOG_PREFIX))

if "variable_3" in locals():
    """
    ``variable_3`` will not be available until after the first run of the
    script, which creates ``test_default_preset``.
    """
    LOG.warn("This is the default script scope plus the things from the custom test_default_preset (variable_1 and variable_2): dir() \n{}".format(dir()))
    LOG.warn("variable_3 [{}]".format(variable_3))# pylint: disable=undefined-variable
    LOG.warn("variable_4 [{}]".format(variable_4))# pylint: disable=undefined-variable
else:
    import core
    LOG.warn("These are the stock presets: scriptExtension.presets \n{}".format(scriptExtension.presets))
    LOG.warn("These are the stock default presets loaded into every script scope: scriptExtension.defaulPresets \n{}".format(scriptExtension.defaultPresets))
    LOG.warn("This is the default script scope plus core, core.log.logging, core.log.LOG_PREFIX, and LOG: dir() \n{}".format(dir()))

    core.JythonExtensionProvider.addValue("variable_1", 1)
    core.JythonExtensionProvider.addValue("variable_2", 2)
    core.JythonExtensionProvider.addPreset("test_non-default_preset", ["variable_1", "variable_2"], False)
    LOG.warn("These are the stock presets plus test_non-default_preset: scriptExtension.presets \n{}".format(scriptExtension.presets))
    LOG.warn("You can just get the values directly without importing, but this will confuse people: scriptExtension.get(\"variable_1\") [{}]".format(scriptExtension.get("variable_1")))# pylint: disable=undefined-variable
    scriptExtension.importPreset("test_non-default_preset")
    LOG.warn("This is the same script scope as before, plus the things from test_non-default_preset (variable_1 and variable_2), after it was imported: dir() \n{}".format(dir()))
    LOG.warn("variable_1 [{}]".format(variable_1))# pylint: disable=undefined-variable
    LOG.warn("variable_2 [{}]".format(variable_2))# pylint: disable=undefined-variable

    core.JythonExtensionProvider.addValue("variable_3", 3)
    core.JythonExtensionProvider.addValue("variable_4", 4)
    core.JythonExtensionProvider.addPreset("test_default_preset", ["variable_3", "variable_4"], True)
    LOG.warn("These are the stock presets plus non-test_default_preset and test_default_preset: scriptExtension.presets \n{}".format(scriptExtension.presets))
    LOG.warn("These are the stock default presets, plus test_default_preset: scriptExtension.defaulPresets \n{}".format(scriptExtension.defaultPresets))
2 Likes

That looks interesting.

Unfortunately, putting this into a dummy script file, the line 22:

core.JythonExtensionProvider.addValue("variable_1", 1)

Gives me a:

Error during evaluation of script 'file:/etc/openhab2/automation/jsr223/python/personal/dummy.py': AttributeError: 'module' object has no attribute 'JythonExtensionProvider' in <script> at line number 22

While the initializations seems to have worked:

2020-01-07 16:03:15.091 [INFO ] [me.core.service.AbstractWatchService] - Loading script 'python/core/components/200_JythonExtensionProvider.py'
2020-01-07 16:03:24.628 [DEBUG] [.jython.core.JythonExtensionProvider] - Start init
2020-01-07 16:03:24.681 [DEBUG] [.jython.core.JythonExtensionProvider] - End init
2020-01-07 16:03:24.699 [DEBUG] [JythonExtensionProvider.scriptLoaded] - Registered service

Which version of Jython are you using? If you are not using the recommended 2.7.0, then try adding this to the beginning of the script…

scriptExtension.importPreset(None)
1 Like

That does the trick, thanks!

I downloaded jython-standalone-2.7.1.jar, since the link to the download page for jython-standalone-2.7.0.jarin the documentation (https://www.jython.org/downloads.html) leads to a 404, the page being meanwhile https://www.jython.org/download, listing 2.7.1 as the “current version”.

Very cool inbuilt feature. I’ll use it, placing simple put / get wrappers around it, just in case.

Thanks, Scott, I wasn’t aware of this option!

Not many people are aware of it, but I’ll get it into the docs. It’s much more useful in Nashorn than Jython.

1 Like

I seem to have “classloader” issues (in Java terms) between objects created in a ScriptExtensions and those created in a script. Logging

isinstance(object, personal.treemanager.Tree)

gives me false when I handed over the class as parameter from a script to a ScriptExtension, but logging the type of the object vs the class parameter type gives me identical strings anyway:

object type: <class 'personal.treemanager.Tree'> / class param: <class 'personal.treemanager.Tree'>

Is that to be expected?

Could you please post an example?

1 Like

Since I’m on a Raspi, your file paths might be slightly different. Here’s a minimal exmple that reporduces the issue:

/etc/openhab2/automation/lib/python/personal/mytreemanager.py:

from core.log import logging, LOG_PREFIX

class Tree(object):

    def __init__(self):
        self.hello_world = "Hello World"

/etc/openhab2/automation/lib/python/personal/myextension.py:

from core.log import logging, LOG_PREFIX

from personal.mytreemanager import Tree

class Myextension(object):
    def __init__(self):
        self.logger = logging.getLogger("{}.{}".format(LOG_PREFIX, self.__class__.__name__))
        self.a_tree = Tree()

    def test(self, the_class):
        self.logger.info("the_class is {}, a_tree is {}".format(the_class, self.a_tree))
        self.logger.info("isinstance(self.a_tree, Tree): {}".format(isinstance(self.a_tree, Tree)))
        self.logger.info("isinstance(self.a_tree, the_class): {}".format(isinstance(self.a_tree, the_class)))

/etc/openhab2/automation/jsr223/python/myextension_init.py:

import core

from core.jsr223 import scope

from core.log import logging, LOG_PREFIX

import personal.myextension
reload(personal.myextension)
from personal.myextension import Myextension


LOG = logging.getLogger("{}.Myextension".format(LOG_PREFIX))


scriptExtension.importPreset("mypreset")

if "myextension" in locals():
    LOG.info("Existing myextension [{}] in preset".format(myextension))
else:
    import core

    theExtension = Myextension()

    core.JythonExtensionProvider.addValue("myextension", theExtension)
    core.JythonExtensionProvider.addPreset("mypreset", ["myextension"], False)

    scriptExtension.importPreset("mypreset")
    LOG.warn("Added extension [{}] to preset".format(theExtension))

/etc/openhab2/automation/jsr223/python/testrule.py:

from core.jsr223 import scope

from core.rules import rule
from core.triggers import when



import personal.mytreemanager
reload(personal.mytreemanager)
from personal.mytreemanager import Tree


scriptExtension.importPreset("mypreset")


@rule("System started", tags="System")
@when("System started")
def testingrule(event):
    testingrule.log.info("**** Begin ***")

    myextension.test(personal.mytreemanager.Tree)

    testingrule.log.info("**** End ***")

Logging output: the output isinstance(self.a_tree, the_class): 0 is the issue: when I pass the class as a parameter, it is not recognized anymore as the type of the object:

2020-01-14 19:29:27.179 [INFO ] [me.core.service.AbstractWatchService] - Loading script 'python/personal/myextension_init.py'
2020-01-14 19:29:39.442 [WARN ] [jsr223.jython.Myextension           ] - Added extension [<personal.myextension.Myextension object at 0x3>] to preset
...
2020-01-14 19:30:53.782 [INFO ] [me.core.service.AbstractWatchService] - Loading script 'python/personal/testingrule.py'
...
2020-01-14 19:31:08.957 [INFO ] [jsr223.jython.System started        ] - **** Begin ***
2020-01-14 19:31:08.993 [INFO ] [jsr223.jython.Myextension           ] - the_class is <class 'personal.mytreemanager.Tree'>, a_tree is <personal.mytreemanager.Tree object at 0x5>
2020-01-14 19:31:09.028 [INFO ] [jsr223.jython.Myextension           ] - isinstance(self.a_tree, Tree): 1
2020-01-14 19:31:09.062 [INFO ] [jsr223.jython.Myextension           ] - isinstance(self.a_tree, the_class): 0
2020-01-14 19:31:09.094 [INFO ] [jsr223.jython.System started        ] - **** End ***

I guess (?) this hint from the helper libraries docs (on the difference between scripts and modules) solves it:

“This means any objects (variables, classes, etc.) previously defined in the script will be lost when the script is reloaded.”

I was not aware that classes count as objects, too. So I’m now assuming that the execution context for my script testrule.py has it’s own definition of personal.mytreemanager.Tree that is simply not the same as the class object created by from personal.mytreemanager import Tree in my module mytreemanager.py.

Just to clear up the nomenclature.
A “class” is the definition for an object. An “object” is an instance of a class.

Think of the classes like blue prints and objects the house built using the blueprint.

I’m not sure about Python, but in some programming languages, everything is an object. In others like Java, there are some things called primitives (int, float, Boolean) that are not objects, but everything else is an object.