Version 1.0 of the Part parser.
Version 1.0 of the Part parser.

file:b/KSP_Parser.py (new)
--- /dev/null
+++ b/KSP_Parser.py
@@ -1,1 +1,667 @@
+#!/usr/bin/python3

+

+#===============================================================================

+# Module-aware parser for KSP part and craft files.

+#===============================================================================

+

+from sys import stderr

+from collections import OrderedDict

+

+global G_ACCEL

+G_ACCEL = 9.81

+

+# This is here to make PyDev stop whining, because it doesn't understand metaclasses properly.

+__classname__ = None

+

+def merge_dicts(*dicts):

+	r = {}

+	for d in dicts:

+		for k, v in d.items():

+			if k not in r:

+				r[k] = v

+			elif isinstance(r[k], dict) and isinstance(v, dict):

+				r[k] = merge_dicts(r[k], v)

+			else: r[k] += v

+	

+	return r

+

+def warn(text):

+	stderr.write("{0}\n".format(text))

+

+class ModularType(type):

+	subclasses = []

+	DerivedValues = {}

+	filter = {}

+	@classmethod

+	def __prepare__(mcls, name, bases):

+		return {'__classname__': name}

+	

+	def __new__(mcls, name, bases, dict):

+		cls = type.__new__(mcls, name, bases, dict)

+		if 'moduleDepends' in dict:

+			for module in dict['moduleDepends']:

+				if module not in mcls.DerivedValues:

+					mcls.DerivedValues[module] = {}

+				mcls.DerivedValues[module] = merge_dicts(mcls.DerivedValues[module], {cls: dict['moduleDepends']})

+		if 'filter' in dict:

+			mcls.filter = merge_dicts(mcls.filter, dict['filter'])

+		if cls not in mcls.subclasses:

+			mcls.subclasses.append(cls)

+		return cls

+

+class Modular(metaclass=ModularType):

+	def __init__(self):

+		self._components = {}

+		self.__setattr__("get{0}Node".format(self.__class__.__name__), self.getComponent)

+	

+	def addComponent(self, comp):

+		if isinstance(comp, Component):

+			o = comp

+		elif isinstance(comp, type):

+			o = comp()

+		else:

+			raise TypeError("Got '{0}', expected Component object or class.".format(comp.__class__.__name__))

+		component_name = o.__class__.__name__

+		if component_name in self._components:

+			return self._components[component_name]

+		

+		self._components[component_name] = o

+		o._container = self

+		

+		if o.__class__ in self.__class__.DerivedValues:

+			for mod, depends in self.__class__.DerivedValues[o.__class__].items():

+				dependsMet = True

+				for depend in depends:

+					if depend.__name__ not in self._components:

+						dependsMet = False

+						break

+				if dependsMet:

+					c = mod()

+					if isinstance(c, mod):

+						self.addComponent(mod)

+		

+		return o

+			

+	

+	def getComponent(self):

+		return self

+	

+	def __getattribute__(self, *args):

+		ex = True

+		rval = None

+		try:

+			rval = object.__getattribute__(self, *args)

+			ex = False

+		except AttributeError as e:

+			for component in self._components.values():

+				try:

+					rval = object.__getattribute__(component, *args)

+					ex = False

+				except AttributeError:

+					pass

+				except Exception as f:

+					e = f

+			if ex:

+				raise e

+		

+		return rval

+

+class Node(Modular):

+	id_count = 0

+	@classmethod

+	def next_id(cls):

+		cls.id_count += 1

+		return cls.id_count

+	

+	def __new__(cls, *args, **kwargs):

+		o = super().__new__(cls)

+		o._ID = cls.next_id()

+		

+		return o

+	

+	def __init__(self):

+		super().__init__()

+		self._Parent = None

+		self._Children = {}

+	

+	@classmethod

+	def _ChangeParent(cls, nod, new_parent):

+		if nod._Parent is not None:

+			nod._Parent._RemoveChildByIndex(nod)

+		new_parent._AddChildByIndex(nod)

+	

+	def _AddChildByIndex(self, child, idx=None):

+		if idx is not None:

+			index = idx

+		else:

+			index = child._ID

+		if child._ID in self._Children:

+			return False

+		if child._Parent is not None:

+			child._Parent._RemoveChildByIndex(idx)

+		child._Parent = self

+		self._Children[index] = child

+		

+		return True

+	

+	def _RemoveChildByIndex(self, Idx):

+		s = "Removing child at index '{0}' from {1}.".format(Idx, self)

+		if Idx in self._Children:

+			child = self._Children[Idx]

+			child._Parent = None

+			self.UpdateBulkGlyph(-child.GetExteriorVolume(), child.Glyph())

+			del self._Children[Idx]

+			child.UnsubscribeAll(self)

+			s += "  Done!"

+			return child

+		s += "  Failed. ({0})".format(self._Children)

+		return False

+

+class Component(Node):

+	HumanName = "ComponentName"

+	HumanUnits = "units"

+	def __init__(self):

+		super().__init__()

+		self._identifier = self.__classname__

+		self._value = 0

+	

+	def getValue(self):

+		return self._value

+	

+	def __str__(self):

+		return " ".join((self.HumanName + ":", str(self.getValue()), self.HumanUnits))

+

+class MultiValueComponent(Component):

+	def __init__(self):

+		super().__init__()

+		self._value = {}

+	

+	def getValueByKey(self, key):

+		return self._value[key]

+	

+	def getValue(self):

+		s = self._value

+		for child in self._container._Children.values():

+			if self.__classname__ in child._components:

+				s = merge_dicts(s, child._components[self.__classname__].getValue())

+		return s

+	

+	def __str__(self):

+		s = []

+		for key, value in self.getValue().items():

+			units = self.HumanUnits

+			if key in self.keyNames:

+				key = self.keyNames[key]

+			if key in self.keyUnits:

+				units = self.keyUnits[key]

+			s.append("{0} ({1}): {2} {3}".format(self.HumanName, key, value, units))

+		return "\n{0: <{w}}".format('', w=self._container._depth * 4).join(s)

+	

+	keyNames = {}

+	keyUnits = {}

+

+class Title(Component):

+	HumanName = "Title"

+	HumanUnits = ""

+	

+	@classmethod

+	def SetName(cls, container, name, *_):

+		o = container.addComponent(cls)

+		if o._value is 0:

+			if isinstance(name, list):

+				name = ';'.join(name)

+			o._value = name.replace('"', "'")

+		return o

+	

+	filter = {

+		'title': [(__classname__, 'SetName')]

+	}

+

+class CrewCapacity(Component):

+	HumanName = "Crew Capacity"

+	HumanUnits = ""

+	

+	@classmethod

+	def SetCapacity(cls, container, cap, *_):

+		o = container.addComponent(cls)

+		o._value = cap

+		return o

+	

+	filter = {

+		'CrewCapacity': [(__classname__, 'SetCapacity')]

+	}

+

+class ResourceStorage(MultiValueComponent):

+	HumanName = "Storage"

+	HumanUnits = "L"

+	

+	@classmethod

+	def AddResourceStorage(cls, container, val, name):

+		o = container.addComponent(cls)

+		o._value[name] = val

+		return o

+	

+	def getValueByKey(self, name):

+		return self._storage[name]

+	

+	filter = {

+		'RESOURCE': {

+			'maxAmount': [(__classname__, 'AddResourceStorage')]

+		}

+	}

+

+class Mass(Component):

+	moduleDepends = [ResourceStorage]

+	HumanName = "Mass"

+	HumanUnits = "t"

+	def __init__(self):

+		super().__init__()

+		

+		self._mass = 0.

+	

+	@classmethod

+	def addMassComponent(cls, container, mass, *_):

+		o = container.addComponent(cls)

+		o._mass += float(mass)

+		return o

+	

+	def getTareMass(self):

+		return self._mass

+	

+	def getMass(self):

+		m = self._mass

+		if ResourceStorage.__name__ in self._container._components:

+			storage = self._container.getResourceStorageNode()

+			for resource, volume in storage._value.items():

+				density = resources._Children[resource]._properties['density']

+				m += volume * density

+		for subpart in self._container._Children.values():

+			if self.__classname__ in subpart._components:

+				m += subpart.getMass()

+		

+		return m

+	

+	def getValue(self):

+		return self.getMass()

+	

+	filter = {

+		'mass': [(__classname__, 'addMassComponent')],

+	}

+

+class Thrust(Component):

+	HumanName = "Thrust"

+	HumanUnits = "kN"

+	def __init__(self):

+		super().__init__()

+		self._ratio = 0.

+	

+	@classmethod

+	def addThrustComponent(cls, container, thrust, *_):

+		o = container.addComponent(cls)

+		o._ratio += float(thrust)

+		if 'Mass' in container._components:

+			container.addComponent(ThrustWeight)

+		return o

+	

+	def getThrust(self):

+		m = self._ratio

+		for subpart in self._container._Children.values():

+			if self.__classname__ in subpart._components:

+				m += subpart.getThrust()

+		

+		return m

+	

+	def getValue(self):

+		return self.getThrust()

+	

+	filter = {

+		'maxThrust': [(__classname__, 'addThrustComponent')]

+	}

+

+class SpecImpulse(MultiValueComponent):

+	HumanName = "Specific Impulse"

+	HumanUnits = "s"

+	def __init__(self):

+		super().__init__()

+		self._value = {}

+	

+	@classmethod

+	def addImpulseComponent(cls, container, specimp_str, *_):

+		o = container.addComponent(cls)

+		coeff, val = specimp_str.split(' ')

+		o._value[coeff] = float(val)

+		return o

+	

+	filter = {

+		'atmosphereCurve': {

+			'key': [(__classname__, 'addImpulseComponent')]

+		}

+	}

+	

+	keyNames = {

+		'0':	'Vac',

+		'1':	'Atm'

+	}

+

+class FuelRatio(MultiValueComponent):

+	HumanName = "Fuel Ratio"

+	HumanUnits = ''

+	

+	@classmethod

+	def addFuelRatio(cls, container, val, name):

+		o = container.addComponent(cls)

+		o._value[name] = val

+	

+	filter = {

+		'PROPELLANT': {

+			'ratio': [((__classname__), 'addFuelRatio')]

+		}

+	}

+

+class ThrustWeight(Component):

+	moduleDepends = [Mass, Thrust]

+	HumanName = 'Thrust-to-Weight Ratio'

+	HumanUnits = ''

+	

+	def getValue(self):

+		return self._container.getThrust() / ( self._container.getMass() * G_ACCEL)

+

+class MassFlow(MultiValueComponent):

+	moduleDepends = [Thrust, SpecImpulse]

+	HumanName = "Mass Flow"

+	HumanUnits = "t/s"

+	

+	def getValue(self):

+		_Thrust = self._container.getThrust()

+		_SpecImpulse = self._container.getSpecImpulseNode().getValue()

+		self.keyNames = self._container.getSpecImpulseNode().keyNames

+		result = {}

+		for key, val in _SpecImpulse.items():

+			result[key] = _Thrust / (val * G_ACCEL)

+		return result

+

+class FuelUsage(MultiValueComponent):

+	moduleDepends = [MassFlow, FuelRatio]

+	HumanName = "Fuel Flow"

+	HumanUnits = "L/s"

+	

+	def getValue(self):

+		result = OrderedDict()

+		_MassFlow = self._container.getMassFlowNode().getValue()

+		_FuelRatios = self._container.getFuelRatioNode().getValue()

+		

+		sumRatios = 0

+		for fuel, ratio in _FuelRatios.items():

+			density = resources._Children[fuel]._properties['density']

+			if density != 0:

+				sumRatios += ratio

+		

+		densityRatios = {}

+		sumDensity = 0

+		for fuel, ratio in _FuelRatios.items():

+			density = resources._Children[fuel]._properties['density']

+			densityRatio = density * ratio / sumRatios

+			densityRatios[fuel] = densityRatio

+			sumDensity += densityRatio

+		

+		for atmo, flow in _MassFlow.items():

+			volFlow = flow / sumDensity

+			for fuel, ratio in _FuelRatios.items():

+				density = resources._Children[fuel]._properties['density']

+				if atmo in SpecImpulse.keyNames:

+					atmo = SpecImpulse.keyNames[atmo]

+				key = "{0} ({1})".format(fuel, atmo)

+				fuelFlow = volFlow * ratio / sumRatios

+				result[key] = fuelFlow

+		

+		return result

+	

+	keyUnits = {

+		'ElectricCharge (Vac)': 'E/s'

+	}

+

+class ElectricRate(Component):

+	moduleDepends = [FuelUsage]

+	HumanName = "Electric Rate"

+	HumanUnits = "E/s"

+	def __init__(self):

+		super().__init__()

+		self._charge_rate = 0.

+	

+	@classmethod

+	def addElectricRateComponent(cls, container, charge_rate, *_):

+		o = container.addComponent(cls)

+		o._value = float(charge_rate)

+		return o

+	

+	def getValue(self):

+		m = self._value

+		for subpart in self._container._Children.values():

+			if self.__classname__ in subpart._components:

+				m += subpart.getElectricRateNode().getValue()

+		# This bit is a dirty, dirty hack to make Ion engines draw power.

+		if 'FuelUsage' in self._container._components:

+			if "ElectricCharge (Vac)" in self._container.getFuelUsageNode().getValue() and m == 0:

+				m = -self._container.getFuelUsageNode().getValue()["ElectricCharge (Vac)"]

+		# KSP determines the input or output of ElectricCharge per module, so we have to, too.

+		inout = 1.

+		if self._container._name in self.charge_users:

+			inout = -1.

+		return m * inout

+	

+	charge_users = [

+		'ModuleCommand',

+		'ModuleWheel'

+	]

+	

+	filter = {

+		'OUTPUT_RESOURCE': {

+			'ElectricCharge': {

+				'rate': [(__classname__, 'addElectricRateComponent')]

+			},

+		},

+		'RESOURCE': {

+			'ElectricCharge': {

+				'rate': [(__classname__, 'addElectricRateComponent')]

+			},

+		},

+		'MODULE': {

+			'ModuleDeployableSolarPanel': {

+				'chargeRate': [(__classname__, 'addElectricRateComponent')]

+			}

+		}

+	}

+

+class Part(Node):

+	@classmethod

+	def createFromFile(cls, filename):

+		with open(filename, 'r', errors="replace") as file:

+			part = cls(file)

+		

+		return part

+	

+	def __init__(self, stream, blocktype='PART', depth = 0):

+		super().__init__()

+		self._type = blocktype

+		self._depth = depth

+		self._properties = {}

+		self._subparts = []

+		self._name = ''

+		self.filter = Component.filter

+		lastkey = ''

+		while True:

+			line = stream.readline()

+			if len(line) == 0:

+				break

+			if line[:1] == '//':

+				continue

+			

+			key, *val = line.split('=', 1)

+			key = key.strip()

+			if key == '{':

+				cls = self.__class__

+				subpart = cls(stream, lastkey, self._depth + 1)

+				if subpart._name == '':

+					subpart._name = subpart._type

+				self._AddChildByIndex(subpart, subpart._name)

+			if key == '}':

+				break

+			lastkey = key

+			if len(val) == 0:

+				continue

+			val = val[0].strip().split(',')

+			if len(val) == 1:

+				val = val[0].strip()

+			if key == "name":

+				self._name = val

+			

+			try:

+				val = float(val)

+			except:

+				pass

+			

+			self._checkFilter(key, val)

+			

+			if key in self._properties:

+				self._properties[key] = [self._properties[key]]

+				self._properties[key].append(val)

+				continue

+			

+			self._properties[key] = val

+	

+	def _checkFilter(self, key, val, filter=None):

+		if filter is None:

+			filter = self.filter

+		if key in filter:

+			for classname, method in filter[key]:

+				func = getattr(globals()[classname], method)

+				if callable(func):

+					func(self, val, self._name)

+			return

+		if self._name in filter:

+			return self._checkFilter(key, val, filter[self._name])

+		if self._type in filter:

+			return self._checkFilter(key, val, filter[self._type])

+	

+	def _AddChildByIndex(self, child, idx=None):

+		if super()._AddChildByIndex(child, idx):

+			for comp in child._components.values():

+				if comp.__classname__ not in self._components:

+					self.addComponent(comp.__class__)

+	

+	def dumpKSPData(self):

+		s = self.__str__() + "\n"

+		for prop, value in self._properties.items():

+			s += '{0: <{w}}'.format('', w=self._depth * 4)

+			s += "{0}: {1}\n".format(prop, repr(value))

+		if len(self._Children) > 0:

+			s += '{0: <{w}}'.format('', w=self._depth * 4)

+			s += 'Subparts\n'

+			for subpart in self._Children.values():

+				s += '{0: <{w}}'.format('', w=self._depth * 4)

+				s += "{0}".format(repr(subpart))

+		return s

+	

+	def getRecursiveString(self):

+		s = self.__str__()

+		if len(self._Children) > 0:

+			s += '{0: <{w}}'.format('', w=self._depth * 4)

+			s += 'Subparts\n'

+			for subpart in self._Children.values():

+				s += '{0: <{w}}'.format('', w=self._depth * 4)

+				s += "{0}".format(subpart.getRecursiveString())

+		return s

+	

+	def __repr__(self):

+		s = self._type

+		if '_name' in self.__dict__:

+			s += " '{0}'".format(self._name)

+		return s

+	

+	def __str__(self):

+		s = self.__repr__()

+		s += ":\n"

+		s += '{0: <{w}}Properties\n'.format('', w=self._depth * 4)

+		for comp in self._components.values():

+			s += '{0: <{w}}'.format('', w=self._depth * 4)

+			s += "{0}\n".format(comp)

+		return s

+

+class Resource(Part):

+	pass

+

+def main():

+	import argparse, os.path

+	

+	parser = argparse.ArgumentParser(description='Parse a KSP part file.')

+	parser.add_argument('files', metavar='FILE', type=str, nargs='+', help='The file or files to be parsed.')

+	

+	args = parser.parse_args()

+	

+	files = args.files

+	

+	globfiles = []

+	remove_indexes = []

+	for filename in files:

+		if filename.count('*') > 0:

+				from glob import glob

+				globfiles += glob(filename)

+				remove_indexes.append(filename)

+	for idx in remove_indexes:

+		files.remove(idx)

+	files += globfiles

+	

+	partdir, _ = os.path.split(files[0])

+	KSPDir = os.sep.join((partdir, '..', '..'))

+	

+	global resources

+	resources = Resource.createFromFile(os.sep.join((KSPDir, 'Resources', 'ResourcesGeneric.cfg')))

+	

+	csv_columns = OrderedDict({o.__name__: [o.HumanName] for o in Component.__subclasses__() if o is not MultiValueComponent})

+	parts = []

+	for filename in files:

+		_, ext = os.path.splitext(filename)

+		

+		if ext.lower() != '.cfg':

+			warn("Skipping file '{0}': Not a cfg file.".format(filename))

+			continue

+		

+		part = Part.createFromFile(filename)

+		for mvc in MultiValueComponent.__subclasses__():

+			if mvc.__name__ in part._components:

+				if mvc.__name__ not in csv_columns:

+					csv_columns[mvc.__name__] = set()

+				for key in getattr(part, "get{0}Node".format(mvc.__name__))().getValue().keys():

+					csv_columns[mvc.__name__].add(key)

+		#print(part)

+		parts.append(part)

+	

+	csv_columns.move_to_end('Title', last=False)

+	header1 = ["{0} {1}".format(o, k) for o in csv_columns for k in csv_columns[o]]

+	

+	print(','.join(header1))

+	

+	for part in parts:

+		row = []

+		for classname in csv_columns:

+			if classname in part._components:

+				node = getattr(part, "get{0}Node".format(classname))()

+			else:

+				node = None

+			classfields = []

+			for key in csv_columns[classname]:

+				if isinstance(node, MultiValueComponent):

+					if key in node.getValue():

+						classfields += [str(node.getValue()[key])]

+					else:

+						classfields += ['']

+				elif node is None:

+					classfields += ['']

+				else:

+					classfields += [str(node.getValue())]

+			row += classfields

+		print(','.join(row))

+

+if __name__ == "__main__":

+	main()