commit-gnue
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[gnue] r8407 - in trunk/gnue-common/src/logic: . adapters


From: reinhard
Subject: [gnue] r8407 - in trunk/gnue-common/src/logic: . adapters
Date: Fri, 14 Apr 2006 18:04:57 -0500 (CDT)

Author: reinhard
Date: 2006-04-14 18:04:56 -0500 (Fri, 14 Apr 2006)
New Revision: 8407

Modified:
   trunk/gnue-common/src/logic/GTriggerCore.py
   trunk/gnue-common/src/logic/NamespaceCore.py
   trunk/gnue-common/src/logic/adapters/Base.py
Log:
Cleaning up.


Modified: trunk/gnue-common/src/logic/GTriggerCore.py
===================================================================
--- trunk/gnue-common/src/logic/GTriggerCore.py 2006-04-14 17:52:16 UTC (rev 
8406)
+++ trunk/gnue-common/src/logic/GTriggerCore.py 2006-04-14 23:04:56 UTC (rev 
8407)
@@ -1,66 +1,89 @@
+# GNU Enterprise Common Library - Trigger Enabled Base Classes
 #
+# Copyright 2000-2006 Free Software Foundation
+#
 # This file is part of GNU Enterprise.
 #
-# GNU Enterprise is free software; you can redistribute it 
+# GNU Enterprise is free software; you can redistribute it
 # and/or modify it under the terms of the GNU General Public
-# License as published by the Free Software Foundation; either 
+# License as published by the Free Software Foundation; either
 # version 2, or (at your option) any later version.
 #
 # GNU Enterprise is distributed in the hope that it will be
-# useful, but WITHOUT ANY WARRANTY; without even the implied 
-# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+# useful, but WITHOUT ANY WARRANTY; without even the implied
+# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 # PURPOSE. See the GNU General Public License for more details.
 #
-# You should have received a copy of the GNU General Public 
-# License along with program; see the file COPYING. If not, 
-# write to the Free Software Foundation, Inc., 59 Temple Place 
+# You should have received a copy of the GNU General Public
+# License along with program; see the file COPYING. If not,
+# write to the Free Software Foundation, Inc., 59 Temple Place
 # - Suite 330, Boston, MA 02111-1307, USA.
 #
-# Copyright 2000-2006 Free Software Foundation
-#
-#
-# FILE:
-# GCoreTrigger.py
-#
-# DESCRIPTION:
-# A base class inherited by GObj.  It's only purpose
-# is to keep all the trigger code in one place instead
-# of tacking some trigger code into GObj directly
-# It's in it's own file to prevent circular imports
-#
-# NOTES:
-#
+# $Id$
+"""
+Base Classes to derive from to make use of the action/trigger system.
+"""
 
-#
-# GTriggerCore
-#
-#
+__all__ = ['GTriggerCore']
+
+
+# =============================================================================
+# Base class for all objects that are exported to trigger namespace
+# =============================================================================
+
 class GTriggerCore:
-  def __init__(self):
-    self._triggerGlobal = 0
-    self._triggerFunctions = {}
+    """
+    Base class for all objects that can be exported to action/trigger
+    namespace.
 
-    self._triggerProperties = {}
+    Descendants can define how they want to be seen in action/trigger code by
+    setting the following properties:
 
-    #
-    # Dictionary representing this object's
-    # local namespace.  Populated as part of
-    # GTriggerNamespace.constructTriggerObject
-    #
-    self._localTriggerNamespace = {}
-    
-    #
-    # Links to functions to be used when
-    # trigger attempts to directly set or get
-    # the objects by name
-    #
-    self._triggerSet = None
-    self._triggerGet = None
+    @ivar _triggerGlobal: If set to True, this object is added to the global
+        namespace. Otherwise, the object is only available as a property of its
+        parent object.
+    @ivar _triggerGet: Can be set to a method that returns the string value of
+        the object. In action/trigger code, "str(object)" will implicitly call
+        this function.
+    @ivar _triggerSet: Can be set to a method that sets the value of this
+        object. In action/trigger code, "parent.object = 'foo' will implicitly
+        call this function.
+    @ivar _triggerFunctions: Dictionary defining the functions this object
+        should present in action/trigger code. Keys in this dictionary are the
+        function names how they should be visible in the action/trigger code.
+        Values are dictionaries, where the key 'function' contains the method
+        to call, and the key 'global' can be set to True to make this a global
+        function.
+    @ivar _triggerProperties: Dictionary defining the properties this object
+        should present in action/trigger code. Keys in this dictionary are the
+        property names how they should be visible in action/trigger code.
+        Values are dictionaries, where the key 'get' contains the method used
+        to read the property, and the key 'set' optionally contains the method
+        used to set the property. Properties with no 'set' key are read only
+        properties.
+    """
 
+    # -------------------------------------------------------------------------
+    # Constructor
+    # -------------------------------------------------------------------------
 
-    #
-    # Dict of triggers that are valid for this specific 
-    # object
-    #
-    self._validTriggers = {}
+    def __init__(self):
+        """
+        Initialize a GTriggerCore instance.
+        """
 
+        self._triggerGlobal = 0
+        self._triggerGet = None
+        self._triggerSet = None
+        self._triggerFunctions = {}
+        self._triggerProperties = {}
+
+        # Dictionary representing this object's
+        # local namespace.  Populated as part of
+        # GTriggerNamespace.constructTriggerObject
+        # FIXME: Belongs into GTriggerExtension?
+        self._localTriggerNamespace = {}
+    
+        # Dict of triggers that are valid for this specific object
+        # FIXME: Belongs into GTriggerExtension?
+        self._validTriggers = {}

Modified: trunk/gnue-common/src/logic/NamespaceCore.py
===================================================================
--- trunk/gnue-common/src/logic/NamespaceCore.py        2006-04-14 17:52:16 UTC 
(rev 8406)
+++ trunk/gnue-common/src/logic/NamespaceCore.py        2006-04-14 23:04:56 UTC 
(rev 8407)
@@ -1,4 +1,7 @@
+# GNU Enterprise Common Library - Namespace Handling
 #
+# Copyright 2000-2006 Free Software Foundation
+#
 # This file is part of GNU Enterprise.
 #
 # GNU Enterprise is free software; you can redistribute it
@@ -16,345 +19,292 @@
 # write to the Free Software Foundation, Inc., 59 Temple Place
 # - Suite 330, Boston, MA 02111-1307, USA.
 #
-# Copyright 2000-2006 Free Software Foundation
-#
-#
-# FILE:
-# NamespaceCore.py
-#
-# DESCRIPTION:
-# Provides the basic classes needed by the generic trigger system
-#
-# NOTES:
-#
-import sys
+# $Id$
+"""
+Classes to build up a namespace object tree from an XML object tree.
+
+Namespace objects are available within action and trigger code. They mirror the
+XML object tree of the document, but the namespace objects are limited to the
+functions and properties that the objects explicitly want to provide.
+"""
+
 import types
-import string
-import copy
+
 from gnue.common.definitions.GObjects import GObj
-from gnue.common.apps import GDebug
 
-from gnue.common.formatting import GTypecast
-from xml.sax import saxutils
+from gnue.common.logic.GTriggerCore import GTriggerCore
 
+__all__ = ['GObjNamespace', 'NamespaceElement']
 
-#######################################################################
-#
-# Trigger/Function namespace classes
-#
-# Classes used in implementing the trigger/function namespaces
-#
-#
+
+# =============================================================================
 # GObjNamespace
-#
-# Manager class for a namespace.  Passed a GObj based
-# tree at __init__ which it uses to construct a new
-# NamespaceElement based tree
-#
-class GObjNamespace(GObj):
-  def __init__(self,objectTree = None, rootName="root"):
-    self._globalNamespace = {'True' : 1,
-                             'False': 0,
-                             }
+# =============================================================================
 
-    self._rname = rootName
+class GObjNamespace:
+    """
+    Helper object to build up a tree of namespace objects. For internal use of
+    the trigger system only.
+    """
 
-    if objectTree:
-      self._globalNamespace[self._rname] = 
self.constructTriggerObject(objectTree)
-    else:
-      assert gDebug(1,'GObjNamespace was passed an empty object tree')
+    # -------------------------------------------------------------------------
+    # Constructor
+    # -------------------------------------------------------------------------
 
-  #
-  # constructTriggerObject
-  #
-  # Travels down thru a GObj based tree and builds a set
-  # of NamespaceElements that will implement the namespace
-  # inside triggers/formulas.
-  #
-  def constructTriggerObject(self, gobjObject, triggerParent=None):
-    triggerObject = None
-    # Some items in a GObj tree may not be GObj based (GContent for instance)
+    def __init__(self, xml_object, rootName = "root"):
+        """
+        Initialize a GObjNamespace instance.
+        """
 
-    if isinstance(gobjObject,GObj) and hasattr(gobjObject,'name'):
-      triggerObject = NamespaceElement(triggerParent)
-      triggerObject._object = gobjObject
+        # FIXME: This should rather check for GRootObj, but that causes a
+        # circular import
+        checktype (xml_object, GObj)
 
-      # Add this triggerObject to global namespace if the GObj requests it
-      if gobjObject._triggerGlobal:
-        self._globalNamespace[gobjObject.name] = triggerObject
+        self._globalNamespace = {}
 
-      # setup get and set functions when they exist in the GObj
-      triggerObject._triggerSet = gobjObject._triggerSet
-      triggerObject._triggerGet = gobjObject._triggerGet
+        self._globalNamespace[rootName] = \
+                self.constructTriggerObject(xml_object)
 
-      # Add any trigger methods defined by GObj
-      if len(gobjObject._triggerFunctions):
-        for item in gobjObject._triggerFunctions.keys():
 
-          if type(gobjObject._triggerFunctions[item]['function']) == 
types.MethodType:
-            object = 
NamespaceFunction(item,gobjObject._triggerFunctions[item]['function'])
-            triggerObject.__dict__[item] = object
+    # -------------------------------------------------------------------------
+    # Construct a namespace object tree for an XML object tree
+    # -------------------------------------------------------------------------
+
+    def constructTriggerObject(self, xml_object):
+        """
+        Construct a namespace object tree from an XML (L{GObject.GObj}) object
+        tree.
+
+        This function creates a L{NamespaceElement} object for each
+        L{GObject.GObj} in the object.
+        """
+
+        # Do not create a namespace object for xml objects that are not trigger
+        # enabled.
+        if not isinstance(xml_object, GTriggerCore):
+            return None
+
+        # Do not create a namespace object for xml objects with no "name"
+        # property.
+        if not hasattr(xml_object, 'name') or xml_object.name is None:
+            return None
+
+        # Create dictionary with methods as defined
+        methods = {}
+        for (name, definition) in xml_object._triggerFunctions.items():
+            function = definition.get('function')
+            assert isinstance(function, types.MethodType)
+            methods[name] = function
             # Add this function to global namespace if the GObj requests it
-            if gobjObject._triggerFunctions[item].get('global',0):
-              self._globalNamespace[item] = object
+            if definition.get('global', False):
+                self._globalNamespace[name] = function
 
-          else:
-            raise 'Only functions are supported in an objects 
_triggerFunctions (%s %s)' % (gobjObject,item)
+        # Create dictionary with properties as defined
+        properties = {}
+        for (name, definition) in xml_object._triggerProperties.items():
+            get_function = definition.get('get')
+            set_function = definition.get('set')
+            assert isinstance(get_function, types.MethodType)
+            if set_function is not None:
+                assert isinstance(set_function, types.MethodType)
+            properties[name] = (get_function, set_function)
 
-            sys.exit()
+        # Create the namespace object
+        namespace_object = NamespaceElement(
+                xml_object = xml_object,
+                get_method = xml_object._triggerGet,
+                set_method = xml_object._triggerSet,
+                methods    = methods,
+                properties = properties)
 
-      # Load the defined __properties__ into this object's
-      # NamespaceElementProperties instance
-      if len(gobjObject._triggerProperties):
-        for item in gobjObject._triggerProperties.keys():
-          if gobjObject._triggerProperties[item].has_key('set'):
-            setFunc = gobjObject._triggerProperties[item]['set']
-          else:
-            setFunc = None
-          
triggerObject._triggerProperties.addProperty(item,gobjObject._triggerProperties[item]['get'],
 setFunc)
+        # Process the children of this xml object
+        if len(xml_object._children):
+            self.__add_children(xml_object._children, namespace_object)
 
-      # Process the children of this Gobj
-      if len(gobjObject._children):
-        self.addChildNames(gobjObject._children, triggerObject)
+        # Add the namespace object to global namespace if the xml object
+        # requests it
+        if xml_object._triggerGlobal:
+            self._globalNamespace[xml_object.name] = namespace_object
 
-      #
-      # populate the GObj's _localTriggerNamespace
-      localNamespace = {'self':triggerObject}
-      localNamespace.update(triggerObject.__dict__)
-      gobjObject._localTriggerNamespace = localNamespace
+        # populate the GObj's _localTriggerNamespace
+        # FIXME: This would only have to be done for all objects that can have
+        # triggers attached, not for all objects that can appear in the trigger
+        # namespace.
+        xml_object._localTriggerNamespace = {'self': namespace_object}
 
-    return triggerObject
+        return namespace_object
 
-  def addChildNames(self, children, triggerObject):
+
+    # -------------------------------------------------------------------------
+    # Add children to a Namespace object matching the children of the GObj
+    # -------------------------------------------------------------------------
+
+    def __add_children(self, children, namespace_object):
+        """
+        Create child namespace objects according to child XML objects
+        """
         for child in children:
-          object = self.constructTriggerObject(child, triggerObject)
+            child_object = self.constructTriggerObject(child)
 
-          # Add this objects children to it's namespace by their name
-          if object:
-            if child.name is not None:
-              # skip on GRPassThru objects
-              if child.name [:2] != '__':
-                triggerObject.__dict__[child.name] = object
-              else:
-                if len(child._children):
-                  self.addChildNames(child._children, triggerObject)
-    
+            # Add this objects children to it's namespace by their name
+            if child_object:
+                # skip on GRPassThru objects
+                if child.name.startswith('__'):
+                    if len(child._children):
+                        self.__add_children(child._children, namespace_object)
+                else:
+                    namespace_object.__dict__[child.name] = child_object
 
-#
-# NamespaceElement
-#
-# Inherits GObj to gain it's parent/child system
-#
-class NamespaceElement(GObj):
-  def __init__(self, parent):
-    GObj.__init__(self,parent)
-    # TODO: Check wether this _parent attribute is really needed, since it
-    # should be a weak reference (or replaced by getParent ())
-    self.__dict__['_parent'] = parent
-    self._triggerProperties = NamespaceElementProperties()
-    self._triggerSet = None
-    self._triggerGet = None
-    self._object = None
 
-  #
-  #
-  #
-  def __repr__(self):
-    try:
-      return "NamespaceElement(%s) at %s" % (self._object.__class__, id(self))
-    except:
-      return "NamespaceElement at %s" % (id(self))
+# =============================================================================
+# Namespace object
+# =============================================================================
 
+class NamespaceElement(object):
+    """
+    Proxy object that represents an object from an XML tree within the
+    action/trigger namespace.
+    """
 
-  #
-  # showTree
-  #
-  # Handy function to dump a rough look at the namespace
-  # doesn't show things nested properly though
-  def showTree(self, indent=0):
-    print '  ' * indent + `self._type` + `self`
-    for item in self.__dict__.keys():
-      if item[:1] != '_':
-        print '  ' * indent + ' :' + item
-    for child in self._children:
-      child.showTree(indent + 2)
+    # -------------------------------------------------------------------------
+    # Constructor
+    # -------------------------------------------------------------------------
 
-  #
-  # __setattr__
-  #
-  # This is called when trying to write something inside a trigger object
-  # namespace.  It checks to see if the var name is already linked to a
-  # NamespaceElement based object and calls that objects _triggerSet if it
-  # exists.
-  #
-  # Example: form.block1.entry1 = "foo"
-  #
-  # The __setattr__ will execute at the block1 and call the functions that
-  # are part of the entry1 object to set it's value
-  #
-  def __setattr__(self, name, value):
-    if self.__dict__.has_key(name) and \
-       isinstance(self.__dict__[name], NamespaceElement):
-     if isinstance(self.__dict__[name], NamespaceElement):
-      if self.__dict__[name]._triggerSet:
-        self.__dict__[name]._triggerSet(value)
-      else:
-        assert gDebug(1,'Trigger attempting to reset a form object')
-    else:
-      self.__dict__[name] = value
+    def __init__(self, xml_object, get_method, set_method, methods,
+            properties):
+        """
+        Initialize a namespace proxy object.
+        """
 
-  #
-  # __getattr__
-  #
-  # Only needed to return the NamespaceElementProperties
-  # object
-  #
-  def __getattr__(self,name):
-    if name == '__properties__':
-        return self._triggerProperties
-    else:
-#      GDebug.printMesg(1,"AttributeError: %s" % name)
-#      print self.__dict__
-      raise AttributeError, '%s' % (name)
+        checktype (xml_object, GTriggerCore)
 
-  #
-  # __str__
-  #
-  # This executes at a different level than the __setattr__
-  # While __setattr__ is executed in the parent object to protect
-  # it's namespace object links, this routine is called by the
-  # object referenced
-  #
-  # Example: foo = form.block1.entry1
-  #
-  # This __str__ would execute against the entry1 object
-  #
-  def __str__(self):
-    if self._triggerGet:
-      return str(self._triggerGet())
-    else:
-      return ""
+        self.__xml_object = xml_object
+        self.__get_method = get_method
+        self.__set_method = set_method
+        self.__methods    = methods
+        self.__properties = properties
 
-  #
-  # __str__
-  #
-  # This executes at a different level than the __setattr__
-  # While __setattr__ is executed in the parent object to protect
-  # it's namespace object links, this routine is called by the
-  # object referenced
-  #
-  # Example: foo = int(form.block1.entry1)
-  #
-  # This __int__ would execute against the entry1 object
-  #
-  def __int__(self):
-    if self._triggerGet:
-      return int(self._triggerGet())
-    else:
-      return 0
 
-  #
-  # __cmp__
-  #
-  # Forces the system to compare the string values of
-  # NamespaceElement objects and not their instances
-  #
-  def __cmp__(self,other):
-    selfvalue = "%s" % self
-    othervalue = "%s" % other
+    # -------------------------------------------------------------------------
+    # Nice string representation
+    # -------------------------------------------------------------------------
 
-    if selfvalue == othervalue:
-      return 0
-    elif selfvalue < othervalue:
-      return -1
-    else:
-      return 1
+    def __repr__(self):
+        return self.__xml_object.name
 
 
-  #
-  # __nonzero__
-  #
-  # Needed to make __len__ function below play nice
-  #
-  def __nonzero__(self):
-    return 1
+    # -------------------------------------------------------------------------
+    # Getting and setting attributes
+    # -------------------------------------------------------------------------
 
-  #
-  # __len__
-  #
-  # Implements len() support
-  #
-  def __len__(self):
-    string =  "%s" % self
-    return len(string)
+    def __getattr__(self, name):
 
-  #
-  # __getitem__
-  #
-  # implements support for sub selections of strings
-  #
-  # example: block1.fieldname[0:4]
-  #
-  def __getitem__(self, key) :
-    string = self.__str__()
-    return string[key.start:key.stop]
+        # Handle methods and properties. Child objects (which are stored in the
+        # native __dict__ of the object) have highest priority.
 
+        if self.__methods.has_key(name):
+            return self.__methods[name]
+        elif self.__properties.has_key(name):
+            return self.__properties[name][0]()
 
-  #
-  # __iter__
-  #
-  # Python iterator support
-  #
-  def __iter__(self):
-     return iter(self._object)
+    # -------------------------------------------------------------------------
 
-#
-# NamespaceFunction
-#
-# Accessor class for functions that are made available in the trigger
-# namespace
-#
-class NamespaceFunction:
-  def __init__(self, name, functionLink):
-    self._name = name
-    self._objectFunction = functionLink
+    def __setattr__(self, name, value):
 
-  def __call__(self, *args, **kwargs):
-    return self._objectFunction(*args, **kwargs)
+        # This is called when trying to write something inside a trigger object
+        # namespace.  It checks to see if the var name is already linked to a
+        # NamespaceElement based object and calls that objects _triggerSet if
+        # it exists.
+        #
+        # Example: form.block1.entry1 = "foo"
+        #
+        # The __setattr__ will execute at the block1 and call the functions
+        # that are part of the entry1 object to set it's value.
+        #
+        # Apart from that, this also handles properties.
 
-#
-# NamespaceElementProperties
-#
-# Accessor class for properties that are made available in this an
-# object's __properties__ namespace
-#
-class NamespaceElementProperties:
-  def __init__(self):
-    self._properties = {}
+        # Directly set for private variables, otherwise we will recurse in
+        # __init__.
+        if name.startswith('_NamespaceElement__'):
+            self.__dict__[name] = value
+            return
 
-  def addProperty(self,name, getFunc, setFunc):
-    self._properties[name] = {'get':getFunc,
-                              'set':setFunc,
-                              }
+        attr = self.__dict__.get(name)
 
-  def __setattr__(self, name, value):
-    # Hack to ensure that self._properties exists
-    if not self.__dict__.has_key('_properties'):
-      self.__dict__['_properties'] = {}
+        if isinstance(attr, NamespaceElement):
+            if attr.__set_method is None:
+                # TODO: Make this a good exception
+                raise "Cannot set value of %s" % name
+            attr.__set_method(value)
 
-    if self._properties.has_key(name):
-      # If none the it's readonly
-      if self._properties[name]['set']:
-        self._properties[name]['set'](value)
-      else:
-        assert gDebug(1,'Attempt to set readonly property :%s' %(name))
-    else:
-      self.__dict__[name] = value
+        elif self.__properties.has_key(name):
+            set_function = self.__properties[name][1]
+            if set_function is None:
+                # TODO: Make this a good exception
+                raise "Cannot set read only property %s" % name
+            set_function(value)
 
-  def __getattr__(self,name):
-    if self.__dict__['_properties'].has_key(name):
-      return self._properties[name]['get']()
-    else:
-      raise AttributeError
+        else:
+            self.__dict__[name] = value
 
 
+    # -------------------------------------------------------------------------
+    # String and Integer values
+    # -------------------------------------------------------------------------
+
+    def __str__(self):
+
+        # This executes at a different level than __setattr__.
+        # While __setattr__ is executed in the parent object to protect its
+        # namespace object links, this routine is called by the referenced
+        # object.
+        #
+        # Example: foo = str(form.block1.entry1)
+        #
+        # This __str__ would execute for the entry1 object.
+
+        if self.__get_method:
+            return str(self.__get_method())
+        else:
+            return self.__xml_object.name
+
+    # -------------------------------------------------------------------------
+
+    def __int__(self):
+
+        if self.__get_method:
+            return int(self.__get_method())
+        else:
+            return 0
+
+    # -------------------------------------------------------------------------
+
+    def __cmp__(self, other):
+
+        # Forces the system to compare the string values of NamespaceElement
+        # objects rather than their instances
+
+        return cmp(str(self), str(other))
+
+    # -------------------------------------------------------------------------
+
+    def __nonzero__(self):
+        return True
+
+    # -------------------------------------------------------------------------
+
+    def __len__(self):
+        return len(str(self))
+
+    # -------------------------------------------------------------------------
+
+    def __getitem__(self, key) :
+        return str(self)[key.start:key.stop]
+
+
+    # -------------------------------------------------------------------------
+    # Iterator support in case the underlying GObj object supports it
+    # -------------------------------------------------------------------------
+
+    def __iter__(self):
+        return iter(self.__xml_object)

Modified: trunk/gnue-common/src/logic/adapters/Base.py
===================================================================
--- trunk/gnue-common/src/logic/adapters/Base.py        2006-04-14 17:52:16 UTC 
(rev 8406)
+++ trunk/gnue-common/src/logic/adapters/Base.py        2006-04-14 23:04:56 UTC 
(rev 8407)
@@ -20,12 +20,10 @@
 #
 # $Id$
 
-import sys
-import traceback
-import string
+import types
 
 from gnue.common.logic.language import ImplementationError, AbortRequest
-from gnue.common.logic.NamespaceCore import NamespaceElement, NamespaceFunction
+from gnue.common.logic.NamespaceCore import NamespaceElement
 
 
 # =============================================================================
@@ -82,7 +80,7 @@
     """
     for (name, value) in addNS.items ():
       if name is not None:
-        if isinstance (value, NamespaceFunction):
+        if isinstance (value, types.MethodType):
           self.bindFunction (name, value, asGlobal)
 
         if isinstance (value, NamespaceElement):





reply via email to

[Prev in Thread] Current Thread [Next in Thread]