Python:KeyValues

From Devicenull's Code

Jump to: navigation, search

This is a basic keyvalues parser. KeyValues are used by the source engine.

import string
class PropertyNotFound(exceptions.Exception):
	def __init__(self,property):
		self.property = property
 
	def __str__(self):
		print "","PropertyNotFound: %s" % (self.property)
 
class KeyNotFound(exceptions.Exception):
	def __init__(self,key):
		self.key = key
 
	def __str__(self):
		print "","KeyNotFound: %s" % (self.key)
 
class Key:
	def __init__(self,name):
		self.name = name
		self.properties = {}
		self.subkeys = []
 
	def getPropertyValue(self,property_name):
		if not self.properties.has_key(property_name):
			raise PropertyNotFound(property_name)
 
		return self.properties[property_name]
 
	def setPropertyValue(self,property_name,property_value):
		self.properties[property_name] = [property_value]
 
	def setMultiplePropertyValue(self,property_name,property_value,offset=-1):
		if offset == -1:
			self.properties[property_name].append(property_value)
		else:
			self.properties[property_name][offset] = property_value
 
	def getSubKey(self,key_name):
		for cur in self.subkeys:
			if cur.name == key_name:
				return cur
 
		raise KeyNotFound(key_name)
 
	def setSubKey(self,key_value):
		self.subkeys.append(key_value)
 
	def __str__(self):
		ret = "\"%s\"\n{" % (self.name)
		#cant believe I'm doing this, but valves parser is stupid
		for cur in self.properties.keys():
			if cur == "GameBin":
				for val in self.properties[cur]:
					ret = "%s\n\t\"%s\"\t\"%s\"" % (ret,cur,val)
 
		for cur in self.properties.keys():
			if cur != "GameBin":
				for val in self.properties[cur]:
					ret = "%s\n\t\"%s\"\t\"%s\"" % (ret,cur,val)
		for cur in self.subkeys:
			child = "%s" % (cur)
			for c in child.split("\n"):
				ret = "%s\n\t%s" % (ret,c)
		ret = "%s\n}" % (ret)
		return ret
 
class KV_Events:
	# This will ocstring once at a minimum for the parent section
	# It is called again for any child section
	def onSectionBegin(self,section_name):
		pass
	# This will ocstring once at a minimum for the parent section
	# It is called when any section ends
	def onSectionEnd(self):
		pass
	# This is called once for each property present
	def onProperty(self,property_name,property_value):
		pass
	# This will be called for any comment present
	def onComment(self,comment):
		pass
 
BEGIN = 0
IN_NAME = 1
DONE_NAME = 2
IN_VALUE = 3
IN_COMMENT = 4
IN_ML_COMMENT = 5
 
class KeyValuesParser:
	def __init__(self,event_receiver):
		self.events = event_receiver
 
	def parse(self,string):
		done = 0
		depth = 0
		name = ""
		value = ""
		state = BEGIN
		total_len = len(string)
		i = 0
		while i < total_len:
			if state == BEGIN:
				if string[i].isspace():
					i = i + 1
					continue	
				elif string[i] == '"':
					# starting a name string
					state = IN_NAME
				elif string[i].isalnum():
					state = IN_NAME
					name = string[i]	
				elif string[i] == '}':
					# a section has ended
					self.events.onSectionEnd()
				elif string[i] == '/' and string[i+1] == '/': 
					# beginning a comment
					# skip the second slash
					i = i + 1
					state = IN_COMMENT
				elif string[i] == '/' and string[i+1] == '*':
					# beginning a multiline comment
					i = i + 1
					state = IN_ML_COMMENT
				else:
					#print 'Unexpected character, ignoring'
					pass
			elif state == IN_NAME:
				if string[i] == '"' or string[i].isspace():
					# done reading a name
					state = DONE_NAME
				else:
					name = "%s%s" % (name,string[i])
			# we are done reading in a name, and trying to figure out what to
			# do next (either section name, or property)
			elif state == DONE_NAME:
				if string[i].isspace():
					i = i + 1
					continue
				elif string[i] == '{':
					# starting a new section, call the handler..
					self.events.onSectionBegin(name)
					# and clear the name
					name = ""
					state = BEGIN
				elif string[i] == '"' :
					state = IN_VALUE
				else:
					state = IN_VALUE
					value = string[i]	
			# we are reading in the text of a value
			elif state == IN_VALUE:
				if string[i] == '/' and string[i+1] == '/':
					self.events.onProperty(name,value.strip())
					name = value = ""
					state = IN_COMMENT
				if string[i] == '"' or string[i] == "\n" or string[i] == "\r":
					self.events.onProperty(name,value)
					name = value = ""
					state = BEGIN
				else:
					value = "%s%s" % (value,string[i])
			elif state == IN_COMMENT:
				if string[i] == "\n" or string[i] == "\r":
					# comment is done!
					self.events.onComment(value)
					value = ""
					state = BEGIN
 
				else:
					value = "%s%s" % (value,string[i])
			elif state == IN_ML_COMMENT:
				if string[i] == '*' and string[i+1] == '/':
					# ML comment is done
					i = i + 1
					self.events.onComment(value)
					value =  ""
					state = BEGIN
				else:
					value = "%s%s" % (value,string[i])
			i = i + 1
 
# Event handler for tree builder class
class KV_TreeBuilder:
	def __init__(self):
		self.parent = None
		self.keystack = []
		self.cur_sectn = None
 
	def onSectionBegin(self,section_name):
		if self.parent == None:
			self.parent = Key(section_name)
			self.keystack.append(self.parent)
			self.cur_sectn = self.parent
		else:
			sectn = Key(section_name)
			self.keystack.append(sectn)
			self.cur_sectn.subkeys.append(sectn)
			self.cur_sectn = sectn
 
	def onSectionEnd(self):
		self.keystack.pop()
 
		if len(self.keystack) > 0:
			self.cur_sectn = self.keystack[-1]
 
	def onProperty(self,property_name,property_value):
		# combine multiple properties into one
		try:
			text = self.cur_sectn.getPropertyValue(property_name)
			self.cur_sectn.setMultiplePropertyValue(property_name,property_value,-1)
		except:		
			self.cur_sectn.setPropertyValue(property_name,property_value)
 
	def onComment(self,comment):
		# comments are stripped from the resulting tree, so do nothing
		pass
 
class KeyValuesTreeBuilder:
	def __init__(self):
		self.events = KV_TreeBuilder()
		self.parser = KeyValuesParser(self.events)
 
	def parse(self,kv_string):
		self.parser.parse(kv_string)
		return self.events.parent