view upreckon/config.py @ 205:166a23999bf7

Added confvar okexitcodemask; changed the validator protocol Callable validators now return three-tuples (number granted, bool correct, str comment) instead of two-tuples (number granted, str comment). They are still allowed to return single numbers. Callable validators must now explicitly raise upreckon.exceptions.WrongAnswer if they want the verdict to be Wrong Answer rather than Partly Correct. okexitcodemask specifies a bitmask ANDed with the exit code of the external validator to get a boolean flag showing whether the answer is to be marked as 'OK' rather than 'partly correct'. The bits covered by the bitmask are reset to zeroes before devising the number of points granted from the resulting number.
author Oleg Oshmyan <chortos@inbox.lv>
date Wed, 17 Aug 2011 20:44:54 +0300
parents dd1f715398f0
children 946e8c09ba12
line wrap: on
line source

# Copyright (c) 2010-2011 Chortos-2 <chortos@inbox.lv>

from __future__ import division, with_statement

from .compat import *
from . import files
from __main__ import options, args

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

import imp, os, posixpath, sys, tempfile

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

defaults_problem = {'kind': 'batch',
                    'usegroups': False,
                    'maxcputime': None,
                    'maxwalltime': None,
                    'maxmemory': None,
                    'match': 'literal',
                    'dummies': (),
                    'testsexcluded': (),
                    'padtests': 0,
                    'paddummies': 0,
                    'taskweight': 100,
                    'groupweight': {},
                    'pointmap': {},
                    'stdio': False,
                    'binary': False,
                    'dummyinname': '',
                    'dummyoutname': '',
                    'tester': None,
                    'maxexitcode': 0,
                    'okexitcodeflag': 0,
                    'inname': '',
                    'ansname': '',
                    'force_zero_exitcode': True}
defaults_global = {'problems': None}
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 nativize_path(portable_path):
	if portable_path.startswith('//:'):
		return portable_path[3:]
	comps = portable_path.split('/')
	for i, comp in enumerate(comps):
		if comp == '..':
			comps[i] = os.path.pardir
		elif comp == '.':
			comps[i] = os.path.curdir
	native_path = os.path.join(*comps)
	if posixpath.isabs(portable_path) and not os.path.isabs(native_path):
		abspath = os.path.abspath(native_path)
		parent = os.path.dirname(abspath)
		while parent != abspath:
			abspath, parent = parent, os.path.dirname(parent)
		native_path = os.path.join(parent, native_path)
	elif not posixpath.isabs(portable_path) and os.path.isabs(native_path):
		native_path = os.path.sep + native_path
	if posixpath.isabs(portable_path) != os.path.isabs(native_path):
		raise ValueError('cannot make native path relative/absolute')
	return native_path

def load_problem(problem_name):
	global builtins
	try:
		dwb = sys.dont_write_bytecode
		sys.dont_write_bytecode = True
	except AttributeError:
		pass
	metafile = files.File(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']
	module = Config(module, globalconf)
	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
	if (not hasattr(module, 'maxcputime') and
	    not hasattr(module, 'maxwalltime') and
	    hasattr(module, 'maxtime')):
		module.maxcputime = module.maxtime
	for name in defaults_problem:
		setattr(module, name, getattr(module, name, defaults_problem[name]))
	if not module.dummyinname:
		module.dummyinname = getattr(module, 'testcaseinname', module.dummyinname)
	if not module.dummyoutname:
		module.dummyoutname = getattr(module, 'testcaseoutname', module.dummyoutname)
	if hasattr(module, 'testee'):
		if isinstance(module.testee, basestring):
			module.path = nativize_path(module.testee)
		else:
			testee = tuple(module.testee)
			module.path = (nativize_path(testee[0]),) + testee[1:]
	elif 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
	if module.tester:
		if isinstance(module.tester, basestring):
			module.tester = nativize_path(module.tester)
		elif not callable(module.tester):
			tester = tuple(module.tester)
			module.tester = (nativize_path(tester[0]),) + tester[1:]
	if not isinstance(module.taskweight, dict):
		try:
			module.taskweight = dict(zip(module.problems, module.taskweight))
		except TypeError:
			pass
	try:
		module.taskweight = module.taskweight[problem_name]
	except KeyError:
		module.taskweight = defaults_problem['taskweight']
	except TypeError:
		pass
	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.maxcputime = module.maxwalltime = 0
	if args:
		module.usegroups = False
		module.tests = args
		module.dummies = ()
	try:
		sys.dont_write_bytecode = dwb
	except NameError:
		pass
	for name in patterns:
		if hasattr(module, name):
			setattr(module, name, getattr(module, name).replace('%', problem_name))
	return module

def load_global():
	global builtins
	try:
		dwb = sys.dont_write_bytecode
		sys.dont_write_bytecode = True
	except AttributeError:
		pass
	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]))
	if hasattr(module, 'tasknames'):
		module.problems = module.tasknames
	# Iterable and mapping taskweights cause re-iteration over problems
	try:
		len(module.problems)
	except Exception:
		module.problems = tuple(module.problems)
	global globalconf
	globalconf = module
	try:
		sys.dont_write_bytecode = dwb
	except NameError:
		pass
	return module