view config.py @ 76:0e5ae28e0b2b

Points are now weighted on a test context basis In particular, this has allowed for simple extensions to the format of testconf to award points to whole test groups without at the same time compromising the future ability of giving partial score for correct but slow solutions. Specifically, the groupweight configuration variable has been added and normally has the format {groupindex: points} where groupindex is the group's index in the tests configuration variable. The backwards incompatible change is that test contexts are no longer guaranteed to learn the score awarded or the maximum possible score for every test case and may instead be notified about them in batches. In other news, the pointmap and groupweight configuration variables can (now) be given as sequences in addition to mappings. (Technically, the distinction currently made is dict versus everything else.) Items of a sequence pointmap/groupweight correspond directly to the test cases/ groups defined in the tests configuration variable; in particular, when groups are used, tests=[1],[2,3];pointmap={1:1,2:2,3:3} can now be written as pointmap=tests=[1],[2,3]. Missing items are handled in the same way in which they are handled when the variable is a mapping. Note that the items of groupweight correspond to whole test groups rather than individual test cases. In other news again, the wording of problem total lines has been changed from '<unweighted> points; weighted score: <weighted>' to '<weighted> points (<unweighted> before weighting)', and group total lines now properly report fractional numbers of points (this is a bug fix).
author Oleg Oshmyan <chortos@inbox.lv>
date Sat, 08 Jan 2011 16:03:35 +0200
parents aea4fc87698a
children d46bd7ee3e69
line wrap: on
line source

#! /usr/bin/env python
# Copyright (c) 2010 Chortos-2 <chortos@inbox.lv>

from __future__ import division, with_statement

try:
	from compat import *
	import files
except ImportError:
	import __main__
	__main__.import_error(sys.exc_info()[1])
else:
	from __main__ import options

if files.ZipArchive:
	try:
		import zipimport
	except ImportError:
		zipimport = None
else:
	zipimport = None

import imp, os, sys, tempfile

__all__ = 'load_problem', 'load_global', 'globalconf'

defaults_problem = {'kind': 'batch',
                    'usegroups': False,
                    'maxtime': None,
                    'maxmemory': None,
                    'dummies': {},
                    'testsexcluded': (),
                    'padtests': 0,
                    'paddummies': 0,
                    'taskweight': 100,
                    'groupweight': {},
                    'pointmap': {},
                    'stdio': False,
                    'dummyinname': '',
                    'dummyoutname': '',
                    'tester': None,
                    'maxexitcode': 0,
                    'inname': '',
                    'ansname': ''}
defaults_global = {'tasknames': None,
                   'force_zero_exitcode': True}
defaults_noerase = {'inname': '%.in',
                    'outname': '%.out',
                    'ansname': '%.ans'}
patterns = ('inname', 'outname', 'ansname', 'testcaseinname',
            'testcaseoutname', 'dummyinname', 'dummyoutname')

class Config(object):
	__slots__ = 'modules', '__dict__'
	
	def __init__(self, *modules):
		self.modules = modules
	
	def __getattr__(self, name):
		for module in self.modules:
			try:
				return getattr(module, name)
			except AttributeError:
				pass
		# TODO: provide a message
		raise AttributeError(name)

# A helper context manager
class ReadDeleting(object):
	__slots__ = 'name', 'file'
	
	def __init__(self, name):
		self.name = name
	
	def __enter__(self):
		try:
			self.file = open(self.name, 'rU')
			return self.file
		except:
			try:
				self.__exit__(None, None, None)
			except:
				pass
			raise
	
	def __exit__(self, exc_type, exc_val, exc_tb):
		self.file.close()
		os.remove(self.name)

def load_problem(problem_name):
	global builtins
	dwb = sys.dont_write_bytecode
	sys.dont_write_bytecode = True
	metafile = files.File('/'.join((problem_name, 'testconf.py')), True, 'configuration')
	module = None
	with CompatBuiltins() as builtins:
		if zipimport and isinstance(metafile.archive, files.ZipArchive):
			try:
				module = zipimport.zipimporter(os.path.dirname(metafile.full_real_path)).load_module('testconf')
			except zipimport.ZipImportError:
				pass
			else:
				del sys.modules['testconf']
		if not module:
			try:
				with metafile.open() as f:
					module = imp.load_module('testconf', f, metafile.full_real_path, ('.py', 'r', imp.PY_SOURCE))
			# Handle the case when f is not a true file object but imp requires one
			except ValueError:
				# FIXME: 2.5 lacks the delete parameter
				with tempfile.NamedTemporaryFile(delete=False) as f:
					inputdatafname = f.name
				metafile.copy(inputdatafname)
				with ReadDeleting(inputdatafname) as f:
					module = imp.load_module('testconf', f, metafile.full_real_path, ('.py', 'r', imp.PY_SOURCE))
			del sys.modules['testconf']
	if hasattr(module, 'padwithzeroestolength'):
		if not hasattr(module, 'padtests'):
			try:
				module.padtests = module.padwithzeroestolength[0]
			except TypeError:
				module.padtests = module.padwithzeroestolength
		if not hasattr(module, 'paddummies'):
			try:
				module.paddummies = module.padwithzeroestolength[1]
			except TypeError:
				module.paddummies = module.padwithzeroestolength
	for name in defaults_problem:
		if not hasattr(globalconf, name):
			setattr(module, name, getattr(module, name, defaults_problem[name]))
	module = Config(module, globalconf)
	if not module.dummyinname:
		module.dummyinname = getattr(module, 'testcaseinname', module.dummyinname)
	if not module.dummyoutname:
		module.dummyoutname = getattr(module, 'testcaseoutname', module.dummyoutname)
	if not hasattr(module, 'path'):
		if hasattr(module, 'name'):
			module.path = module.name
		elif sys.platform != 'win32':
			module.path = os.path.join(os.path.curdir, problem_name)
		else:
			module.path = problem_name
	for name in 'pointmap', 'groupweight':
		oldmap = getattr(module, name)
		if isinstance(oldmap, dict):
			newmap = {}
			for key in oldmap:
				if not options.legacy and isinstance(key, basestring):
					newmap[key] = oldmap[key]
				else:
					try:
						for k in key:
							newmap[k] = oldmap[key]
					except TypeError:
						newmap[key] = oldmap[key]
			setattr(module, name, newmap)
	if options.no_maxtime:
		module.maxtime = 0
	sys.dont_write_bytecode = dwb
	for name in patterns:
		if hasattr(module, name):
			setattr(module, name, getattr(module, name).replace('%', problem_name))
	return module

def load_global():
	global builtins
	dwb = sys.dont_write_bytecode
	sys.dont_write_bytecode = True
	metafile = files.File('testconf.py', True, 'configuration')
	module = None
	with CompatBuiltins() as builtins:
		if zipimport and isinstance(metafile.archive, files.ZipArchive):
			try:
				module = zipimport.zipimporter(os.path.dirname(metafile.full_real_path)).load_module('testconf')
			except zipimport.ZipImportError:
				pass
			else:
				del sys.modules['testconf']
		if not module:
			try:
				with metafile.open() as f:
					module = imp.load_module('testconf', f, metafile.full_real_path, ('.py', 'r', imp.PY_SOURCE))
			# Handle the case when f is not a true file object but imp requires one
			except ValueError:
				# FIXME: 2.5 lacks the delete parameter
				with tempfile.NamedTemporaryFile(delete=False) as f:
					inputdatafname = f.name
				metafile.copy(inputdatafname)
				with ReadDeleting(inputdatafname) as f:
					module = imp.load_module('testconf', f, metafile.full_real_path, ('.py', 'r', imp.PY_SOURCE))
			del sys.modules['testconf']
	for name in defaults_global:
		setattr(module, name, getattr(module, name, defaults_global[name]))
	if not options.erase:
		for name in defaults_noerase:
			setattr(module, name, getattr(module, name, defaults_noerase[name]))
	global globalconf
	globalconf = module
	sys.dont_write_bytecode = dwb
	return module