tools/configcollector.py

Tue, 27 Jul 2021 09:56:06 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Tue, 27 Jul 2021 09:56:06 +0300
changeset 114
4e03b0e2a29f
parent 41
0abada2a9802
permissions
-rwxr-xr-x

added build-time test for line length

#!/usr/bin/env python3
# coding: utf-8
#
#	Copyright 2015 - 2017 Teemu Piippo
#	All rights reserved.
#
#	Redistribution and use in source and binary forms, with or without
#	modification, are permitted provided that the following conditions
#	are met:
#
#	1. Redistributions of source code must retain the above copyright
#	   notice, this list of conditions and the following disclaimer.
#	2. Redistributions in binary form must reproduce the above copyright
#	   notice, this list of conditions and the following disclaimer in the
#	   documentation and/or other materials provided with the distribution.
#	3. Neither the name of the copyright holder nor the names of its
#	   contributors may be used to endorse or promote products derived from
#	   this software without specific prior written permission.
#
#	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#	"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
#	TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
#	PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
#	OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
#	EXEMPLARY, OR CONSEQUENTIAL DAMAGES(INCLUDING, BUT NOT LIMITED TO,
#	PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
#	PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
#	LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT(INCLUDING
#	NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
#	SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

from argparse import ArgumentParser
from collections import OrderedDict
import caseconversions
import outputfile

# These types are passed by value
passbyvalue = {'int', 'bool', 'float', 'double', 'qreal'}

def deduce_type(value):
	'''
		Try to determine the type of value from the value itself.
	'''
	if value in('true', 'false'):
		return 'bool'
	elif value.startswith(('"', 'R"')) and value.endswith('"'):
		return 'QString'

	try:
		int(value)
		return 'int'
	except:
		pass

	try:
		float(value)
		return 'double'
	except:
		pass

	if value.endswith('f'):
		try:
			float(value[:-1])
			return 'float'
		except:
			pass

	raise ValueError('unable to deduce type of %r' % value)

class ConfigCollector:
	def __init__(self, args):
		self.declarations = OrderedDict()
		self.qtTypes = set()
		self.args = args

	def collect(self, filename):
		with open(filename) as file:
			for linenumber, line in enumerate(file, 1):
				try:
					line = line.strip()
					if line and not line.startswith('#'):
						from re import search
						match = search('^option (\w+) = (.+)$', line)
						if not match:
							raise ValueError('unable to parse: %r' % line)
						name, value = match.groups()
						match = search(r'^([a-zA-Z0-9_<>]+)\s*\{(.*)\}$', value)
						try:
							typename = match.group(1)
						except:
							typename = deduce_type(value)
						self.declare(name, typename, value)
				except ValueError as error:
					from sys import stderr, exit
					print(str.format(
						'{file}:{line}: {error}',
						file = filename,
						line = linenumber,
						error = str(error),
					), file = stderr)
					exit(1)
		# Sort the declarations in alphabetical order
		self.declarations = OrderedDict(sorted(self.declarations.items(), key = lambda t: t[1]['name']))
		# Fill in additional information
		for declaration in self.declarations.values():
			declaration['readgate'] = caseconversions.convert_case(declaration['name'], style='java')
			declaration['writegate'] = 'set' + caseconversions.convert_case(declaration['name'], style='camel')
			declaration['togglefunction'] = 'toggle' + caseconversions.convert_case(declaration['name'], style='camel')
			if declaration['type'] in passbyvalue:
				declaration['typereference'] = declaration['type']
			else:
				declaration['typereference'] = 'const %s&' % declaration['type']

	def declare(self, name, typename, default):
		from re import findall
		if name in self.declarations:
			raise ValueError('Attempted to redeclare %r' % name)
		self.declarations[name] = {
			'name': name,
			'type': typename,
			'default': default
		}
		# Keep a file of any Qt types, we'll need to #include them.
		self.qtTypes.update(findall(r'Q\w+', typename))

	def writeHeader(self, device):
		device.write('#pragma once\n')
		device.write('#include <QMap>\n')
		device.write('#include <QSettings>\n')
		device.write('#include <glm/glm.hpp>\n')
		for qtType in sorted(self.qtTypes):
			device.write('#include <%s>\n' % qtType)
		device.write('#include "libraries.h"\n')
		device.write('\n')
		formatargs = {}
		write = lambda value: device.write(value)
		write('class Configuration : private QSettings\n')
		write('{\n')
		write('public:\n')
		write('\tusing QSettings::QSettings;\n');
		write('\tbool exists(const QString& name);\n')
		write('\tQVariant value(const QString& name);\n')
		write('\tQVariant setValue(const QString& name);\n')
		for declaration in self.declarations.values():
			write('\t{type} {readgate}();\n'.format(**declaration))
		for declaration in self.declarations.values():
			write('\tvoid {writegate}({typereference} value);\n'.format(**declaration))
		for declaration in filter(lambda declaration: declaration['type'] == 'bool', self.declarations.values()):
			write('\tvoid {togglefunction}();\n'.format(**declaration))
		write('private:\n')
		write('\tusing QSettings::value;\n')
		write('\tusing QSettings::setValue;\n')
		write('\tstatic const QMap<QString, QVariant>& defaults();\n')
		write('};\n')
	
	def writeSource(self, device, headername):
		device.write('#include <QSet>\n')
		device.write('#include <QSettings>\n')
		device.write('#include <QVariant>\n')
		device.write('#include "%s/mainwindow.h"\n' % (self.args.sourcedir))
		device.write('#include "%s"\n' % headername)
		device.write(
			'const QMap<QString, QVariant>& Configuration::defaults()\n'
			'{\n'
			'\tstatic const QMap<QString, QVariant> defaults {\n'
		)
		for declaration in self.declarations.values():
			device.write('\t\t{{"{name}", QVariant::fromValue<{type}>({default})}},\n'.format(**declaration))
		device.write('\t};\n'
			'\treturn defaults;\n'
			'}\n'
			'\n')
		device.write('bool Configuration::exists(const QString& name)\n')
		device.write('{\n')
		device.write('\treturn defaults().contains(name);\n')
		device.write('}\n')
		device.write('\n')
		device.write('QVariant Configuration::value(const QString& name)\n'
			'{\n'
			'\treturn this->value(name, Configuration::defaults().value(name));\n'
			'}\n')
		device.write('\n')
		for declaration in self.declarations.values():
			device.write('{type} Configuration::{readgate}()\n'.format(**declaration))
			device.write('{\n')
			device.write('\tstatic const QVariant defaultvalue = QVariant::fromValue<{type}>({default});\n'.format(**declaration))
			device.write('\treturn this->value("{name}", defaultvalue).value<{type}>();\n'.format(**declaration))
			device.write('}\n')
			device.write('\n')
		for declaration in self.declarations.values():
			device.write('void Configuration::{writegate}({typereference} value)\n'.format(**declaration))
			device.write('{\n')
			device.write('\tif (value != {default})\n'.format(**declaration))
			device.write('\t\tthis->setValue("{name}", QVariant::fromValue<{type}>(value));\n'.format(**declaration))
			device.write('\telse\n')
			device.write('\t\tthis->remove("{name}");\n'.format(**declaration))
			device.write('}\n')
			device.write('\n')
		for declaration in filter(lambda declaration: declaration['type'] == 'bool', self.declarations.values()):
			device.write('void Configuration::{togglefunction}()\n'.format(**declaration))
			device.write('{\n')
			device.write('\t{writegate}(not {readgate}());\n'.format(**declaration))
			device.write('}\n')
			device.write('\n')

def main():
	parser = ArgumentParser(description='Collects a list of configuration objects')
	parser.add_argument('input')
	parser.add_argument('--header', required=True)
	parser.add_argument('--source', required=True)
	parser.add_argument('--sourcedir', required=True)
	args = parser.parse_args()
	collector = ConfigCollector(args)
	collector.collect(args.input)
	from outputfile import OutputFile
	header = OutputFile(args.header)
	source = OutputFile(args.source)
	collector.writeSource(source, headername=args.header)
	collector.writeHeader(header)
	header.save(verbose = True)
	source.save(verbose = True)

if __name__ == '__main__':
	main()

mercurial