#
# pyconfig.py:
#
# This module essentially performs introspection on the
# Python interpreter itself, to discover which language
# features are available.
#
# Usage notes:
#   The recommended way to use this module is:
#
#           from gnosis.pyconfig import pyconfig  # or 'import *'
#
#           if pyconfig.Have_Iterators():
#                 ...
#
#   Using the 'pyconfig' object like this auto-caches results so
#   you can use the function calls inline without a speed penalty
#   of rerunning the test each time.
#
# Background/rationale:
#
# At first glance, this module seems crazy -- why not just check
# sys.version_info? Well, for example:
#
#	o You could be running under a nonstandard/alternative
#	  Python implementation (e.g. Jython, or maybe a cut-down
#	  embedded version that eliminated some features) and you
#	  want to dynamically figure out what is available.
#
#	o It seems more self-documenting to write something like:
#
#		  if Have_Iterators():
#				 ... do something with iterators ...
#
#		Than to write:
#
#		  if sys.version_info[0] >= 2 and sys.version_info[1] >= 2:
#				 ... do something with iterators ...
#
#	  In other words, your code now says "here is the capability
#	  I need", making it more maintainable down the road than
#	  a hardcoded version match. Plus, it is more robust in
#	  the presence of a nonstandard interpreter.
#
#	o You're writing an installer, and want to pick different
#	  modules to install, based on platform.
#
#	o Some oddball (or future) version of Python might break
#	  sys.version_info (expand the number of values, etc.), so you
#	  don't want to rely on it.
#

# This code may be freely used, modified and distributed, but I'd
# appreciate a copy of any fixes/enhancements.
#
#	 -- Frank McIngvale (frankm, at hiwaay dot net)

# (Possible) TODO: Enhance tests to verify correctness of results, not
#				   just that each testcase compiles & runs OK.

# Note: Compatibility with Python 1.5 is required here.
import __builtin__, string

# make 'import *' safe
__all__ = ['pyconfig']

# FYI, there are tests for these PEPs:
#
# PEP 227 - nested scopes
# PEP 232 - function attributes
# PEP 236 - __future__ directive
# PEP 207 - rich comparisons
# PEP 279 - enumerate
# PEP 218 - builtin sets
# PEP 234 - iterators
# PEP 255 - generators
# PEP 237 - promoting ints to longs
# PEP 238 - true division
# PEP 289 - generator expressions
# PEP 252/253 - newstyle classes (check existance via Have_Object())
# PEP 318 - decorators
# PEP 322 - reverse iteration
# PEP 328 - multiline imports
# PEP 292 - $ string substitution
# PEP N/A - augmented assignment ('+=')
# PEP N/A - list comprehensions
# PEP N/A - import NAME as OTHERNAME
# PEP N/A - string methods
# PEP N/A - unicode
# PEP N/A - basestring class
# PEP N/A - longs > sys.maxint in range()
# PEP N/A - call dict() with keywords
# PEP N/A - 'bool' as a class
# PEP 308 - Conditional expressions
# PEP 328 - Relative imports
# PEP 341 - Unified try/except/finally
# PEP 342 - Sending values to generators
#         - .throw() in generators
#         - .close() in generators
# PEP 343 - The 'with' statement
# PEP 352 - The BaseException class
# PEP 357 - The __index__ method
#
# Notable PEP's not tested:
# PEP 205 - weak refs (use Have_Module("weakref") instead)
# PEP 208 - new coercion model
#			(can you test this at the Python level?)
# PEP 217 - interactive display hook
# PEP 229 - build system [involves building Python itself]
# PEP 230 - warning framework [don't see a need to test for this]
# PEP 235 - case sensitivity in import
#			[would rather not create tempfiles in order to test this]
# PEP 237 - Warnings on int->long promotion [don't see a need to test]
# PEP 241 - metadata in packages
#			[this is a convention for packages, has nothing to do w/language]
# PEP 324 - subprocess -- use Have_Module("subprocess")
# PEP 327 - Decimal type -- use Have_Module("decimal")
# PEP 331 - locale independent float/string conversion
#			[does this only affect C extensions?]
# PEP 309 - functional module (use Have_Module("functional") instead)
# PEP 314 - Distutils module change
# PEP 338 - Affects command-line usage
# PEP 353 - C API, not visible from Python

# when developing new tests, it's a good idea to turn this
# on to make sure the correct exceptions are being raised.
SHOW_DEBUG_INFO = 0

# compile/run is split into two steps, in case we need to
# check "can compile" vs. "can run" somewhere down the road
def compile_code( codestr ):
	# compiler module is not available under older Pythons,
	# so I adapted the following from py_compile.py
	codestr = string.replace(codestr, "\r\n","\n")
	codestr = string.replace(codestr,"\r","\n")	
	if codestr and codestr[-1] != '\n':
		codestr = codestr + '\n'

	return __builtin__.compile(codestr, 'dummyname', 'exec')

def can_run_code( codestr ):
	try:
		# run with empty globals & locals so test results won't
		# be affected by the callers namespace, and the tests
		# don't write into the callers namespace
		eval( compile_code(codestr), {}, {} )
		return 1
	except Exception,exc:
		if SHOW_DEBUG_INFO:
			print "RUN EXC ",str(exc)
			
		return 0

def Have_Module( module_name ):
	"Check for the existance of the named module."
	return can_run_code("import %s\ndel %s\n" % (module_name,module_name))

def Have_TrueFalse():
	"Does this Python support True/False builtins?"
	return can_run_code('a = True')

def Have_ObjectClass():
	"""
	Does this Python have builtin 'object' class?
	(This also means that you have newstyle classes, as
	well as builtin classes 'dict', 'list', 'int', etc.)
	"""
	return can_run_code('class foo(object):\n\tpass')

slots_testcode = """
class foo(object):
	__slots__ = ('aaa','bbb')

f = foo()
try:
	f.ccc = 1
	raise "BAD SLOTS"
except AttributeError:
	pass
"""

def Have_Slots():
	"""Does this Python recognize object __slots__?"""
	return can_run_code(slots_testcode)

def Have_BoolClass():
	"""
	Does this Python have 'bool' as a class (instead of a function)?
	"""
	# A little different than above: Since bool can't be a
	# base class, and there is also a bool function in 2.2,
	# you have to be careful to not grab the wrong thing.
	return can_run_code('issubclass(bool, object)')

def IsLegal_BaseClass( classname ):
	"Is it legal to subclass the given classname?"
	return can_run_code('class f(%s): pass' % classname)

# formatting is a little easier if the code fragments are
# sitting outside the functions that use them
iter_test_code = """
class iter_test:
	def __iter__(self): return self
	def next(self): raise StopIteration

for n in iter_test():
	pass
"""

def Have_Iterators():
	"Does this Python support iterators?"
	return can_run_code(iter_test_code)

def Have_Future():
	"Does this Python support 'from __future__ ...'?"
	return can_run_code('import __future__')

generator_test_code = """
def test_generate(N): yield N
test_generate(4)
"""

def Have_Generators():
	"Does this Python have generators?"
	
	s = ''	
	# older Pythons don't have __future__, and the way __future__
	# works, you can't put it inside a try..except, so I have to
	# modify the code dynamically.
	if Have_Future(): 
		# need this for Python 2.2 to support generators
		s = s + "from __future__ import generators\n"

	s = s + generator_test_code
	
	return can_run_code(s)

def Have_TrueDivision():
	"Does this Python support true division (i.e. 1/2 == 0.5)?"
	
	s = ''
	# see notes above
	if Have_Future():
		# needed for Python 2.3 to support true division
		s = s + "from __future__ import division\n"

	s = s + "if (1/2) == 0: raise Exception('True division does not work')"
	
	return can_run_code(s)

nested_scope_testcode = """
def fff():
	def ggg():
		return ggg

	return ggg()
fff()
"""

def Have_NestedScopes():
	"Does this Python support nested scopes?"
	
	s = ''
	# see notes above
	if Have_Future():
		# needed for Python 2.0 to support nested scopes
		s = s + "from __future__ import nested_scopes\n"
		
	s = s + nested_scope_testcode
	return can_run_code(s)

def Have_Unicode():
	"Does this Python support Unicode strings?"
	return can_run_code("s = u'hello world'")

def Have_StringMethods():
	"Does this Python support string methods? (e.g. ''.lower())"
	return can_run_code("s = ''.lower()")

augassign_test_code = """
i = 1
i += 2
"""

def Have_AugmentedAssignment():
	"Does this Python support augmented assignment ('+=')?"
	return can_run_code(augassign_test_code)

def Have_ListComprehensions():
	"Does this Python support list comprehensions?"
	return can_run_code("l = [x*2 for x in range(1)]")

def Have_ImportAs():
	"Does this Python support 'import module AS name'?"
	# pick a small, Python-only module that exists everywhere
	return can_run_code("import statvfs as abcdefg")

richcomp_test_code = """
class foo:
	def __lt__(self,o):
		return 10

if (foo() < 1) < 10:
	raise Exception # __lt__ wasn't called
"""

def Have_RichComparison():
	"Does this Python support rich comparison methods? (__lt__, etc.)"
	return can_run_code(richcomp_test_code)

funcattr_test_code = """
def f(): pass
f.test = 1
"""

def Have_FunctionAttributes():
	"Does this Python support function attributes?"
	return can_run_code(funcattr_test_code)

def Have_UnifiedLongInts():
	"Does this Python auto-promote ints to longs?"
	return can_run_code("a = 2**64 # becomes a long in Python 2.2+")

def Have_Enumerate():
	"Does this Python support 'enumerate()'?"
	return can_run_code("a = enumerate([1,2,3])")

def Have_ReverseIteration():
	"Does this Python support 'reversed()'?"
	return can_run_code("a = reversed([1,2,3])")

def Have_Basestring():
	"Does this Python support the basestring type?"
	return can_run_code("a = basestring")

def Have_LongRanges():
	"Does this Python support longs (>sys.maxint) in ranges?"
	return can_run_code("range(2**64,2**65,2**64)")

def Have_DictKWArgs():
	"Does this Python support dict() keywords? (i.e. dict(a=1) ==> {'a':1}"
	return can_run_code("dict(a=1,b=2)")

def Have_BuiltinSet():
	"Does this Python have builtin set objects?"
	return can_run_code("set('abcde')")

def Have_GeneratorExpressions():
	"Does this Python support generator expressions?"
	return can_run_code("(x for x in range(1))")

decorator_test_code = """
def foo(f): return f
@foo
def g(): pass
"""

def Have_Decorators():
	"Does this Python support function/method decorators?"
	return can_run_code(decorator_test_code)

multiline_imp_test_code = """
# use same small module as in ImportAs test
from statvfs import (F_BSIZE,
					 F_BLOCKS, F_BFREE,
					 F_FILES)
"""

def Have_MultilineImports():
	"Does this Python support parenthesised multiline imports?"
	return can_run_code(multiline_imp_test_code)

def Have_StringDollarSubst():
	"Does this Python support $-substitution in strings?"
	return can_run_code('from string import Template')

assigndoc_test = """
def f(): pass
f.__doc__ = 'aaa'
"""

def Can_AssignDoc():
	"Old Pythons (pre 1.5?) can't assign to __doc__."
	return can_run_code(assigndoc_test)

def Have_ConditionalExpr():
	"Does this Python support conditional expressions ('A = B if X else Y')"
	return can_run_code('a = 1 if 0 else 2')

def Have_RelativeImports():
	"Does this Python support relative imports? ('from .. import aaa')"
	
	# ugh, I don't like to do it this way, but the only other way I can
	# think of to test this would involve creating a test package and
	# that would violate the "don't create tempfiles" philosophy here
	try:
		eval( compile_code("from . import aaa"), {}, {} )		
	except SyntaxError:
		return 0
	except ValueError:
		return 1
	except:
		raise Exception("Unexpected error value")

tef_test_code = """
try: a = [1,2,3][4]
except IndexError: pass
else: pass
finally: pass
"""
def Have_UnifiedTryExceptFinally():
	"Does this Python have unified try/except/finally?"
	return can_run_code(tef_test_code)

# this could be a lot simpler, but I decided to check for
# correctness too
gensend_testcode = """
def sendgen(i):
	while i:
		val = (yield i)		
		if val is not None: i = val
		else: i += 1
			
g = sendgen(10)
g.next()
g.send(20)
if g.next() != 21:
	raise Exception('aaa')
"""
def Have_GeneratorSend():
	"Can you send() values to generators in this Python?"
	return can_run_code(gensend_testcode)

# like above, could be simpler
genthrow_testcode = """
def sendgen(i):
	while i:
		yield i
		i = i-1
		
g = sendgen(10)
if not hasattr(g,'throw'):
	raise Exception()
"""
def Have_GeneratorThrow():
	"Do generators support .throw()?"
	return can_run_code(genthrow_testcode)

# like above, could be simpler
genclose_testcode = """
def sendgen(i):
	while i:
		yield i
		i = i-1
		
g = sendgen(10)
if not hasattr(g,'close'):
	raise Exception()
"""
def Have_GeneratorClose():
	"Do generators support .close()?"
	return can_run_code(genclose_testcode)

withstmt_testcode = """
from __future__ import with_statement
from threading import Lock # even Python 1.5 has threading.Lock
with Lock(): pass
"""
def Have_With():
	"Does this Python have the 'with' statement?"
	return can_run_code(withstmt_testcode)

def Have_BaseException():
	"Does this Python have the BaseException class?"
	return can_run_code("class a(BaseException): pass")

indexmeth_testcode = """
class A:
	def __index__(self): return 2
		
a = [1,2,3]
if a[A()] != 3:
	raise Exception("AAA")
"""
def Have_IndexMethod():
	"Does this Python support the __index__ method?"
	return can_run_code(indexmeth_testcode)
	
#-- End of test code, start of support functions --

class invariant_func_cache:
	"""
	Cache results of invariant function calls.
	
	`func` is a function (taking zero or more arguments) whose
	results are always the same, given the same inputs.
	"""
	def __init__(self,func):
		self.func = func
		self.results = {} # results cache, keyed by args
		
	def __call__(self,*args):
		if not self.results.has_key(args):
			self.results[args] = apply(self.func, args)
			
		return self.results[args]

import sys

class pyconfig_wrapper:
	"""
	All functions here should be invariant, so create
	an invariant_func_cache object the first time each
	function is called to cache the results.

	If this assumption is ever broken, we can always add
	checks in getattr for the non-invariants.
	"""
	def __init__(self):
		# cache of function names to invariant_func_cache objects
		self.cache = {}
		
	def __getattr__(self,name):
		# is there a better way to write this?
		func = getattr(sys.modules[self.__class__.__module__],name,None)
		if func is None:
			raise Exception("Unknown function pyconfig.%s" % name)
		
		if not self.cache.has_key(name):
			self.cache[name] = invariant_func_cache(func)
			
		return self.cache[name]

# there is just one of these
pyconfig = pyconfig_wrapper()

#================================================================
#
# Demo showing how to run all the tests.
#
#================================================================

def runtest(msg, test, *args):
	r = apply(test, args)
	print "%-40s %s" % (msg,['no','yes'][r])	

if __name__ == '__main__':

	import sys,os

	# show banner w/version
	try:
		v = sys.version_info
		print "Python %d.%d.%d-%s [%s, %s]" % (v[0],v[1],v[2],str(v[3]),
											   os.name,sys.platform)
	except:
		# Python 1.5 lacks sys.version_info
		print "Python %s [%s, %s]" % (string.split(sys.version)[0],
									  os.name,sys.platform)

	# Python 1.5
	print "     ** Python 1.5 features **"
	runtest("Can assign to __doc__?", pyconfig.Can_AssignDoc)
	
	# Python 1.6
	print "     ** Python 1.6 features **"
	runtest("Have Unicode?", pyconfig.Have_Unicode)
	runtest("Have string methods?", pyconfig.Have_StringMethods)

	# Python 2.0
	print "     ** Python 2.0 features **"	
	runtest("Have augmented assignment?", pyconfig.Have_AugmentedAssignment)
	runtest("Have list comprehensions?", pyconfig.Have_ListComprehensions)
	runtest("Have 'import module AS ...'?", pyconfig.Have_ImportAs)

	# Python 2.1
	print "     ** Python 2.1 features **"	
	runtest("Have __future__?", pyconfig.Have_Future)
	runtest("Have rich comparison?", pyconfig.Have_RichComparison)
	runtest("Have function attributes?", pyconfig.Have_FunctionAttributes)
	runtest("Have nested scopes?", pyconfig.Have_NestedScopes)

	# Python 2.2
	print "     ** Python 2.2 features **"		
	runtest("Have True/False?", pyconfig.Have_TrueFalse)	
	runtest("Have 'object' type?", pyconfig.Have_ObjectClass)
	runtest("Have __slots__?", pyconfig.Have_Slots)
	# note: can use this to test for existance of any arbitrary module,
	#		I just picked 'compiler' since it showed up in 2.2
	runtest("Have 'compiler' module?", pyconfig.Have_Module, 'compiler')
	runtest("Have iterators?", pyconfig.Have_Iterators)
	runtest("Have generators?", pyconfig.Have_Generators)
	runtest("Have true division?", pyconfig.Have_TrueDivision)
	runtest("Unified longs/ints?", pyconfig.Have_UnifiedLongInts)
	
	# Python 2.3
	print "     ** Python 2.3 features **"		
	runtest("Have enumerate()?", pyconfig.Have_Enumerate)
	runtest("Have basestring?", pyconfig.Have_Basestring)
	runtest("Longs > maxint in range()?", pyconfig.Have_LongRanges)
	runtest("dict() accepts keywords?", pyconfig.Have_DictKWArgs)
	runtest("Have 'bool' class?", pyconfig.Have_BoolClass)
	if Have_BoolClass():
		runtest("bool is a baseclass [expect 'no']?", pyconfig.IsLegal_BaseClass, 'bool')
	
	# Python 2.4
	print "     ** Python 2.4 features **"		
	runtest("Have builtin set?", pyconfig.Have_BuiltinSet)
	runtest("Have function/method decorators?", pyconfig.Have_Decorators)
	runtest("Have multiline imports?", pyconfig.Have_MultilineImports)
	runtest("Have generator expressions?", pyconfig.Have_GeneratorExpressions)
	runtest("Have reverse iteration?", pyconfig.Have_ReverseIteration)
	
	runtest("Have string $-substitution?", pyconfig.Have_StringDollarSubst)
	
	# Python 2.5
	print "     ** Python 2.5 features **"
	runtest("Have conditional expressions?", pyconfig.Have_ConditionalExpr)
	runtest("Have relative imports?", pyconfig.Have_RelativeImports)
	runtest("Have unified try/except/finally?", pyconfig.Have_UnifiedTryExceptFinally)
	runtest("Can send() values to generators?", pyconfig.Have_GeneratorSend)
	runtest("Do generators support .throw()?", pyconfig.Have_GeneratorThrow)
	runtest("Do generators support .close()?", pyconfig.Have_GeneratorClose)
	runtest("Have 'with' statement?", pyconfig.Have_With)
	runtest("Have BaseException?", pyconfig.Have_BaseException)
	runtest("Have __index__ method?", pyconfig.Have_IndexMethod)


