KSP_PartParser: Added logic for .craft files. It's messy.
KSP_PartParser: Added logic for .craft files. It's messy.
KSP_Components:
Taught MassFlow and SpecImpulse to play nicely.
Added several craft-relevant derived properties.
Modular:
Added a generic "setValue" method, which is currently unused.
Rounded off the number values to 6 sigfigs.
KSP_Part:
Added Craft.
Part.__init__ will now correctly parse parts within .craft files.
KSPP_Config:
Added globally-accessible configuration values.

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

+

+class Config():

+	_instance = None

+	def __new__(cls):

+		if not isinstance(cls._instance, cls):

+			cls._instance = object.__new__(cls)

+			cls._instance._configDict = {}

+		return cls._instance

+	

+	def __getitem__(self, idx):

+		return self._configDict[idx]

+	

+	def __setitem__(self, idx, val):

+		self._configDict[idx] = val

+		

--- a/KSP_Components.py
+++ b/KSP_Components.py
@@ -18,17 +18,27 @@
 	HumanName = "Title"

 	HumanUnits = ""

 	

+	def __init__(self):

+		super().__init__()

+		self._value = ''

+	

 	@classmethod

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

 		o = container.addComponent(cls)

-		if o._value is 0:

+		if o._value is '':

 			if isinstance(name, list):

 				name = ';'.join(name)

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

 		return o

 	

-	filter = {

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

+	def getValue(self):

+		if self._value == '':

+			return super().getValue()

+		return self._value

+	

+	filter = {

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

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

 	}

 

 class CrewCapacity(Component):

@@ -146,6 +156,17 @@
 		o._value[coeff] = float(val)

 		return o

 	

+	def getValue(self):

+		if self._container._type in ('CRAFT', 'PART'):

+			_Thrust = self._container.getThrustNode().getValue()

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

+			results = {}

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

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

+			return results

+		else:

+			return super().getValue()

+	

 	filter = {

 		'atmosphereCurve': {

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

@@ -186,6 +207,8 @@
 	HumanUnits = "t/s"

 	

 	def getValue(self):

+		if self._container._type != 'MODULE':

+			return super().getValue()

 		_Thrust = self._container.getThrust()

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

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

@@ -285,4 +308,67 @@
 			}

 		}

 	}

+

+class Acceleration(Component):

+	moduleDepends = [Mass, Thrust]

+	HumanName = "Min. Acceleration"

+	HumanUnits = 'm/s²'

+	

+	

+	def getValue(self):

+		_Thrust = self._container.getThrustNode().getValue()

+		_Mass = self._container.getMassNode().getMass()

+		return _Thrust / _Mass

+

+class BurnTime(MultiValueComponent):

+	moduleDepends = [FuelUsage, ResourceStorage]

+	HumanName = "Burn Time"

+	HumanUnits = 's'

+	

+	def getValue(self):

+		_FuelUsage = self._container.getFuelUsageNode().getValue()

+		_FuelStore = self._container.getResourceStorageNode().getValue()

+		result = {}

+		for key, usage in _FuelUsage.items():

+			fuel, condition = key.rsplit(' ', 1)

+			if fuel not in _FuelStore:

+				storage = 0

+			else:

+				storage = _FuelStore[fuel]

+			if fuel == 'ElectricCharge':

+				_ElectricRate = self._container.getElectricRateNode().getValue()

+				usage = -_ElectricRate

+			time = storage / usage

+			if time < 0:

+				time = float('inf')

+			result[key] = time

+		return result

+

+class DeltaV(MultiValueComponent):

+	moduleDepends = [Acceleration, BurnTime]

+	HumanName = "Min. delta-V"

+	HumanUnits = "m/s"

+	

+	def getValue(self):

+		_Accel = self._container.getAccelerationNode().getValue()

+		_BurnS = self._container.getBurnTimeNode().getValue()

+		

+		result = {}

+		for key, time in _BurnS.items():

+			result[key] = _Accel * time

+		return result

+

+class TotalImpulse(MultiValueComponent):

+	moduleDepends = [Thrust, BurnTime]

+	HumanName = "Available Impulse"

+	HumanUnits = 'kN·s'

+	

+	def getValue(self):

+		_Thrust = self._container.getThrustNode().getValue()

+		_BurnS = self._container.getBurnTimeNode().getValue()

+		

+		result = {}

+		for key, time in _BurnS.items():

+			result[key] = _Thrust * time

+		return result

 

--- a/KSP_Part.py
+++ b/KSP_Part.py
@@ -1,14 +1,16 @@
 #!/usr/bin/python3

 

-__all__ = ['Part', 'Resource']

+__all__ = ['Part', 'Resource', 'Craft']

 

 from Modular import Node, Component

+from KSPP_Config import Config

 

 class Part(Node):

+	parts = {}

 	@classmethod

-	def createFromFile(cls, filename):

+	def createFromFile(cls, filename, depth=0):

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

-			part = cls(fileStream)

+			part = cls(fileStream, depth=depth)

 		

 		return part

 	

@@ -30,7 +32,12 @@
 			key, *val = line.split('=', 1)

 			key = key.strip()

 			if key == '{':

-				cls = self.__class__

+				if lastkey == 'PART':

+					cls = Part

+				if lastkey == 'RESOURCE_DEFINITION':

+					cls = Resource

+				else:

+					cls = self.__class__

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

 				if subpart._name == '':

 					subpart._name = subpart._type

@@ -45,6 +52,20 @@
 				val = val[0].strip()

 			if key == "name":

 				self._name = val

+			if key == "ship":

+				self._name = val

+			

+			if self._type == 'PART' and key == "part":

+				name, guid = val.rsplit('_', 1)

+				self._name = guid

+				name = name.replace(".", "_")

+				if name in Part.parts:

+					o = Part.parts[name]

+				else:

+					from os import sep

+					filename = sep.join((Config()['partsDir'], name, 'part.cfg'))

+					o = Part.createFromFile(filename)

+				self._AddChildByIndex(o)

 			

 			try:

 				val = float(val)

@@ -59,6 +80,9 @@
 				continue

 			

 			self._properties[key] = val

+		

+		if self._name != '':

+			self.__class__.parts[self._name] = self

 	

 	def _checkFilter(self, key, val, componentFilter=None):

 		if componentFilter is None:

@@ -90,7 +114,7 @@
 			s += 'Subparts\n'

 			for subpart in self._Children.values():

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

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

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

 		return s

 	

 	def getRecursiveString(self):

@@ -102,6 +126,12 @@
 				s += '{0: <{w}}'.format('', w=self._depth * 4)

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

 		return s

+	

+	@classmethod

+	def get(cls, idx):

+		if idx in cls.parts:

+			return cls.parts[idx]

+		raise KeyError("{0}: No such part.".format(repr(idx)))

 	

 	def __repr__(self):

 		s = self._type

@@ -133,4 +163,32 @@
 		if idx in cls._resource_parent._Children:

 			return cls._resource_parent._Children[idx]

 		raise KeyError("{0}: No such resource.".format(repr(idx)))

+

+class Craft(Part):

+	@classmethod

+	def createFromFile(cls, filename):

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

+			c = cls(fileStream, blocktype='CRAFT')

+		

+		return c

+	

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

+		super().__init__(stream, blocktype, depth)

+	

+	def __str__(self):

+		s = super().__str__()

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

+		s += "{0} Parts:\n".format(len(self._Children))

+		partsCount = {}

+		longestTitle = 0

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

+			title = child.getTitleNode().getValue()

+			if title not in partsCount:

+				partsCount[title] = 0

+			partsCount[title] += 1

+			longestTitle = max(longestTitle, len(title))

+		for title, count in partsCount.items():

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

+			s += "{0: <{w}}{1: >3}\n".format(title + ':', count, w=longestTitle + 1)

+		return s

 

--- a/KSP_PartParser.py
+++ b/KSP_PartParser.py
@@ -7,6 +7,7 @@
 from Modular import Component, MultiValueComponent

 from KSP_Part import *

 import KSP_Components

+from KSPP_Config import Config

 from _tools import warn

 from collections import OrderedDict

 

@@ -52,9 +53,10 @@
 	

 	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.')

-	parser.add_argument('-k', '--ksp-dir', metavar='DIR', type=str, nargs=1, help='Path to the KSP main directory.')

-	parser.add_argument('-r', '--resource-file', metavar='FILE', type=str, nargs=1, help='The ResourcesGeneric.cfg file to be used.')

+	parser.add_argument('-k', '--ksp-dir', metavar='DIR', type=str, help='Path to the KSP main directory.')

+	parser.add_argument('-r', '--resource-file', metavar='FILE', type=str, help='The ResourcesGeneric.cfg file to be used.')

 	parser.add_argument('-c', '--csv', action='store_true', help='Creates CSV output for all parsed part files.')

+	parser.add_argument('-p', '--parts-dir', metavar='DIR', type=str, help='Path to the parts dir for use with craft parsing.')

 	

 	args = parser.parse_args()

 	

@@ -62,47 +64,64 @@
 	

 	globfiles = []

 	remove_indexes = []

+	preloadParts = False

 	for filename in files:

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

 				from glob import glob

 				globfiles += glob(filename)

 				remove_indexes.append(filename)

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

+		if ext == '.craft' and not preloadParts:

+			preloadParts = True

 	for idx in remove_indexes:

 		files.remove(idx)

 	files += globfiles

 	

 	if args.ksp_dir is not None:

-		KSPDir = args.ksp_dir[0]

+		Config()['KSPDir'] = args.ksp_dir

 	else:

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

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

+		Config()['partsDir'] = partdir

+		Config()['KSPDir'] = os.sep.join((Config()['partsDir'], '..', '..'))

+	

+	if args.parts_dir is not None:

+		Config()['partsDir'] = args.parts_dir

 	

 	if args.resource_file is not None:

-		resourceFile = args.resource_file[0]

+		Config()['resourceFile'] = args.resource_file

 	else:

-		resourceFile = os.sep.join((KSPDir, 'Resources', 'ResourcesGeneric.cfg'))

+		Config()['resourceFile'] = os.sep.join((Config()['KSPDir'], 'Resources', 'ResourcesGeneric.cfg'))

 	

-	global resources

-	resources = Resource.createFromFile(resourceFile)

+	Resource.createFromFile(Config()['resourceFile'])

 	

-	parts = []

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

+		# Let's parse some parts.

+	

+	if preloadParts:

+		from glob import glob

+		preloadFiles = glob(os.sep.join((Config()['partsDir'], '*', 'part.cfg')))

+		for filename in preloadFiles:

+			Part.createFromFile(filename, 1)

+	

+	results = []

 	for filename in files:

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

 		

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

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

-			continue

+		ext = ext.lower()

+		if ext == '.cfg':

+			cls = Part

+		elif ext == '.craft':

+			cls = Craft

 		

-		part = Part.createFromFile(filename)

+		result = cls.createFromFile(filename)

 		

-		#print(part)

-		parts.append(part)

+		results.append(result)

 	

 	if args.csv:

-		printCSV(parts)

+		printCSV(results)

 	else:

-		for part in parts:

-			print(part)

+		for result in results:

+			print(result)

 

 if __name__ == "__main__":

 	main()


file:a/Modular.py -> file:b/Modular.py
--- a/Modular.py
+++ b/Modular.py
@@ -10,6 +10,7 @@
 

 from _tools import merge_dicts

 from sys import modules

+from collections import OrderedDict

 

 class ModularType(type):

 	subclasses = {}

@@ -43,7 +44,7 @@
 

 class Modular(metaclass=ModularType):

 	def __init__(self):

-		self._components = {}

+		self._components = OrderedDict()

 	

 	def addComponent(self, comp):

 		if isinstance(comp, Modular):

@@ -137,14 +138,11 @@
 		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

 			del self._Children[Idx]

-			s += "  Done!"

 			return child

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

 		return False

 

 class Component(Node):

@@ -155,11 +153,27 @@
 		self._identifier = self.__classname__

 		self._value = 0

 	

+	def getTareValue(self):

+		return self._value

+	

+	def setValue(self, value):

+		self._value = value

+	

 	def getValue(self):

-		return self._value

+		s = self._value

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

+			if self.__class__.__name__ in child._components:

+				childCompNode = child._components[self.__class__.__name__]

+				s += childCompNode.getValue()

+		return s

 	

 	def __str__(self):

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

+		_val = self.getValue()

+		try:

+			_val = "{0:.6g}".format(_val)

+		except:

+			pass

+		return "{0}: {1} {2}".format(self.HumanName, _val, self.HumanUnits)

 

 class MultiValueComponent(Component):

 	def __init__(self):

@@ -179,6 +193,10 @@
 	def __str__(self):

 		s = []

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

+			try:

+				value = "{0:.6g}".format(value)

+			except:

+				pass

 			units = self.HumanUnits

 			if key in self.keyNames:

 				key = self.keyNames[key]