'''
NameSpace v0.04:

A "NameSpace" is an object wrapper around a _base dictionary
which allows chaining searches for an 'attribute' within that
dictionary, or any other namespace which is defined as part
of the search path (depending on the downcascade variable, is
either the hier-parents or the hier-children).

You can assign attributes to the namespace normally, and read
them normally. (setattr, getattr, a.this = that, a.this)

I use namespaces for writing parsing systems, where I want to
differentiate between sources (have multiple sources that I can
swap into or out of the namespace), but want to be able to get
at them through a single interface.  There is a test function
which gives you an idea how to use the system.

In general, call NameSpace(someobj), where someobj is a dictionary,
a module, or another NameSpace, and it will return a NameSpace which
wraps up the keys of someobj.  To add a namespace to the NameSpace,
just call the append (or hier_addchild) method of the parent namespace
with the child as argument.

### NOTE: if you pass a module (or anything else with a dict attribute),
names which start with '__' will be removed.  You can avoid this by
pre-copying the dict of the object and passing it as the arg to the
__init__ method.

### NOTE: to properly pickle and/or copy module-based namespaces you
will likely want to do: from mcf.utils import extpkl, copy_extend

### Changes:
	97.05.04 -- Altered to use standard hierobj interface, cleaned up
	interface by removing the "addparent" function, which is reachable
	by simply appending to the __parent__ attribute, though normally
	you would want to use the hier_addchild or append functions, since
	they let both objects know about the addition (and therefor the 
	relationship will be restored if the objects are stored and unstored)
	
	97.06.26 -- Altered the getattr function to reduce the number of
	situations in which infinite lookup loops could be created
	(unfortunately, the cost is rather high).  Made the downcascade
	variable harden (resolve) at init, instead of checking for every
	lookup. (see next note)
	
	97.08.29 -- Discovered some _very_ weird behaviour when storing
	namespaces in mcf.store dbases.  Resolved it by storing the 
	__namespace_cascade__ attribute as a normal attribute instead of
	using the __unstore__ mechanism... There was really no need to
	use the __unstore__, but figuring out how a functions saying
	self.__dict__['__namespace_cascade__'] = something
	print `self.__dict__['__namespace_cascade__']` can print nothing
	is a bit beyond me. (without causing an exception, mind you)

	97.11.15 Found yet more errors, decided to make two different 
	classes of namespace.  Those based on modules now act similar
	to dummy objects, that is, they let you modify the original
	instead of keeping a copy of the original and modifying that.
	
	98.03.15 -- Eliminated custom pickling methods as they are no longer
	needed for use with Python 1.5final
	
	98.03.15 -- Fixed bug in items, values, etceteras with module-type
	base objects.
'''
import copy, types, string

from mcf.utils import hierobj

class NameSpace(hierobj.Hierobj):
	'''
	An hierarchic NameSpace, allows specification of upward or downward
	chaining search for resolving names
	'''
	def __init__(self, val = None, parents=None, downcascade=1,children=[]):
		'''
		A NameSpace can be initialised with a dictionary, a dummied
		dictionary, another namespace, or something which has a __dict__
		attribute.
		Note that downcascade is hardened (resolved) at init, not at
		lookup time.
		'''
		hierobj.Hierobj.__init__(self, parents, children)
		self.__dict__['__downcascade__'] = downcascade # boolean
		if val is None:
			self.__dict__['_base'] = {}
		else:
			if type( val ) == types.StringType:
				# this is a reference to a module which has been pickled
				val = __import__( val, {},{}, string.split( val, '.') )
			try:
				# See if val's a dummy-style object which has a _base
				self.__dict__['_base']=copy.copy(val._base)
			except (AttributeError,KeyError):
				# not a dummy-style object... see if it has a dict attribute...
				try:
					if type(val) != types.ModuleType:
						val = copy.copy(val.__dict__)
				except (AttributeError, KeyError):
					pass
				# whatever val is now, it's going to become our _base...
				self.__dict__['_base']=val
		# harden (resolve) the reference to downcascade to speed attribute lookups
		if downcascade: self.__dict__['__namespace_cascade__'] = self.__childlist__
		else: self.__dict__['__namespace_cascade__'] = self.__parent__
	def __setattr__(self, var, val):
		'''
		An attempt to set an attribute should place the attribute in the _base
		dictionary through a setitem call.
		'''
		# Note that we use standard attribute access to allow ObStore loading if the
		# ._base isn't yet available.
		try:
			self._base[var] = val
		except TypeError:
			setattr(self._base, var, val)
	def __getattr__(self,var):
##		print '__getattr__', var
		return self.__safe_getattr__(var, {}) # the {} is a stopdict

	def __safe_getattr__(self, var,stopdict):
		'''
		We have a lot to do in this function, if the attribute is an unloaded
		but stored attribute, we need to load it.  If it's not in the stored
		attributes, then we need to load the _base, then see if it's in the 
		_base.
		If it's not found by then, then we need to check our resource namespaces
		and see if it's in them.
		'''
		# we don't have a __storedattr__ or it doesn't have this key...
		if var != '_base':
			try:
				return self._base[var]
			except (KeyError,TypeError), x:
				try:
					return getattr(self._base, var)
				except AttributeError:
					pass
		try: # with pickle, it tries to get the __setstate__ before restoration is complete
			for cas in self.__dict__['__namespace_cascade__']:
				try:
					stopdict[id(cas)] # if succeeds, we've already tried this child
					# no need to do anything, if none of the children succeeds we will
					# raise an AttributeError
				except KeyError:
					stopdict[id(cas)] = None
					return cas.__safe_getattr__(var,stopdict)
		except (KeyError,AttributeError):
			pass
		raise AttributeError, var
	def items(self):
		try:
			return self._base.items()
		except AttributeError:
			pass
		try:
			return self._base.__dict__.items()
		except AttributeError:
			pass
	def keys(self):
		try:
			return self._base.keys()
		except AttributeError:
			pass
		try:
			return self._base.__dict__.keys()
		except AttributeError:
			pass
	def has_key( self, key ):
		try:
			return self._base.has_key( key)
		except AttributeError:
			pass
		try:
			return self._base.__dict__.has_key( key)
		except AttributeError:
			pass
	def values(self):
		try:
			return self._base.values()
		except AttributeError:
			pass
		try:
			return self._base.__dict__.values()
		except AttributeError:
			pass

	def __getinitargs__(self):
		if type( self._base ) is types.ModuleType:
			base = self._base.__name__
		else:
			base = self._base
		return (base, self.__parent__, self.__downcascade__, self.__childlist__)
	def __getstate__(self):
		return None
	def __setstate__(self,*args):
		pass
	def __deepcopy__(self, memo=None):
		d = id(self)
		if memo is None:
			memo = {}
		elif memo.has_key(d):
			return memo[d]
		if type(self._base) == types.ModuleType:
			rest = tuple(map( copy.deepcopy, (self.__parent__, self.__downcascade__, self.__childlist__) ))
			new = apply(self.__class__, (self._base,)+rest )
		else:
			new = tuple(map( copy.deepcopy, (self._base, self.__parent__, self.__downcascade__, self.__childlist__) ))
		return new
##	def __del__( self, id=id ):
##		print 'del namespace', id( self )
			

def test():
	import string
	a = NameSpace(string)
	del(string)
	a.append(NameSpace({'a':23,'b':42}))
	import math
	a.append(NameSpace(math))
	print 'The returned object should allow access to the attributes of the string,\nand math modules, and two simple variables "a" and "b" (== 23 and42 respectively)'
	return a


