get_variable now allows a default value to be passed to the call.

If the key is not found, the default value is returned. set_variable
exception handling has been fixed to return an exception in the
current call frame.

Many wesnoth module functions now release the python GIL when it is
both safe to do so and the function call takes long enough where it
also makes sense. A new global boolean variable, 'restricted' is now
set before the user AI script is invoked. This variable indicates if
it is running inside of a restricted python environment or not. A new
class of unrestricted scripts are now listed. Previously only scripts
which have '#!WPY' at the top are allowed. If only allow safe python
scripts is disabled, scripts which start with #!UNSAFE_WPY are also
shown to users. This allows AI authors to specifically target either a
restricted or unrestricted environment. New "system" class attributes
are exposed in the restricted environment. These include; '__call__',
'__copy__', '__deepcopy__', '__doc__', '__name__', '__repr__' and
'__str__', in addition to the old __init__ method.
This commit is contained in:
Greg Copeland 2008-06-30 13:44:59 +00:00
parent 36a973f478
commit 2618cc3d15
4 changed files with 68 additions and 20 deletions

View File

@ -29,7 +29,27 @@ Version 1.5.1+svn:
* Removed persistance from team configuration (bug: #10916)
* Made automaticaly generated macro reference easier to naviagate and link to
(patch: #1076)
* Python AI
* get_variable now allows a default value to be passed to the call. If the
key is not found, the default value is returned.
* set_variable exception handling has been fixed to return an exception in
the current call frame.
* Many wesnoth module functions now release the python GIL when it is both
safe to do so and the function call takes long enough where it also
makes sense. More changes coming.
* A new global boolean variable, 'restricted' is now set before the user AI
script is invoked. This variable indicates if it is running inside of
a restricted python environment or not.
* A new class of unrestricted scripts are now listed. Previously only scripts which have
#!WPY at the top are allowed. If only allow safe python scripts is disabled,
scripts which start with #!UNSAFE_WPY are also shown to users. This allows
AI authors to specifically target either a restricted or unrestricted environment.
* New "system" class attributes are exposed in the restricted environment. These
include; '__call__', '__copy__', '__deepcopy__', '__doc__', '__name__',
'__repr__' and '__str__', in addition to the old __init__ method.
* ValueError can now be caught.
Version 1.5.1:
* campaigns:
* Descent into Darkness:
@ -88,7 +108,7 @@ Version 1.5.1:
* Try/Except clauses are now allowed. A subset of builtin exceptions are available.
ArithmeticError, AssertionError, AttributeError, BaseException,
StopIteration, IndexError, KeyError, NameError, RuntimeError,
RuntimeWarning, ZeroDivisionError
RuntimeWarning, and ZeroDivisionError
* Exceptions can now be raised by user code.
* terrains:
* Fixed city village not being alias of the village terrain type; this was

View File

@ -23,7 +23,7 @@ def include(matchob):
except IOError:
pass
else:
raise safe.SafeException("Could not include %s." % name)
raise safe.SafeException("Could not import '%s'." % name)
r += code

View File

@ -45,7 +45,8 @@ _NODE_ATTR_OK = []
# Expanded to allow repr, str, call, and doc. These are commonly overloaded
# to provided fundamental functionality. Without __call__ support, most
# categories of decorators are simply impossible.
_STR_OK = [ '__call__', '__doc__', '__init__', '__name__', '__repr__', '__str__' ]
_STR_OK = [ '__call__', '__copy__', '__deepcopy__', '__doc__',
'__init__', '__name__', '__repr__', '__str__' ]
# If we put '__' in _STR_NOT_CONTAIN, then we can't have defacto private data
_STR_NOT_CONTAIN = []
@ -79,15 +80,15 @@ def _check_ast(code):
_BUILTIN_OK = [
'__debug__','quit','exit',
'Warning',
'Warning', 'restricted',
'None','True','False',
'abs', 'bool', 'callable', 'chr', 'cmp', 'complex', 'dict', 'divmod', 'filter',
'float', 'frozenset', 'hash', 'hex', 'int', 'isinstance', 'issubclass', 'len',
'list', 'long', 'map', 'max', 'min', 'object', 'oct', 'ord', 'pow', 'range',
'repr', 'round', 'set', 'slice', 'str', 'sum', 'super', 'tuple', 'xrange', 'zip',
'ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'StopIteration',
'IndexError', 'KeyError', 'NameError', 'RuntimeError', 'RuntimeWarning',
'ZeroDivisionError'
'ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'Exception',
'IndexError', 'KeyError', 'NameError', 'RuntimeError', 'RuntimeWarning', 'StopIteration',
'ValueError', 'ZeroDivisionError'
]
_BUILTIN_STR = [
@ -100,6 +101,7 @@ def _builtin_fnc(k):
return fnc
_builtin_globals = None
_builtin_globals_r = None
def _builtin_init():
global _builtin_globals, _builtin_globals_r
if _builtin_globals != None: return
@ -107,14 +109,22 @@ def _builtin_init():
r = _builtin_globals = {}
for k in __builtin__.__dict__.keys():
v = None
if k in _BUILTIN_OK: v = __builtin__.__dict__[k]
elif k in _BUILTIN_STR: v = ''
else: v = _builtin_fnc(k)
if k in _BUILTIN_OK:
v = __builtin__.__dict__[k]
elif k in _BUILTIN_STR:
v = ''
else:
v = _builtin_fnc(k)
r[k] = v
def _builtin_destroy():
_builtin_init()
for k,v in _builtin_globals.items():
__builtin__.__dict__[k] = v
def _builtin_restore():
for k,v in _builtin_globals_r.items():
__builtin__.__dict__[k] = v
@ -147,8 +157,8 @@ def safe_exec_op( code, context=None ):
# Wrapper allowing safe_exec to be dynamically controlled
# from wesnoth binary.
def safe_exec( code, context=None, runSafe=True ):
# Allow the AI to know if it is restricted or not
context["restricted"] = runSafe
context[ 'restricted' ] = runSafe
if runSafe:
safe_exec_op( code, context )

View File

@ -1006,6 +1006,7 @@ static PyObject* wrapper_team_is_enemy(wesnoth_team* team, void* /*closure*/)
// Find side number of team
int side = 0;
Py_BEGIN_ALLOW_THREADS
for (size_t t = 0; t < running_instance->get_teams().size(); t++) {
if (team->team_ == &running_instance->get_teams()[t]) {
side = 1 + t;
@ -1014,6 +1015,8 @@ static PyObject* wrapper_team_is_enemy(wesnoth_team* team, void* /*closure*/)
}
result = running_instance->current_team().is_enemy(side) == true ? 1 : 0;
Py_END_ALLOW_THREADS
return Py_BuildValue(INTVALUE, result);
}
@ -1705,8 +1708,13 @@ PyObject* python_ai::wrapper_set_variable(PyObject* /*self*/, PyObject* args)
PyObject* python_ai::wrapper_get_variable(PyObject* /*self*/, PyObject* args)
{
char const *variable;
if (!PyArg_ParseTuple(args, STRINGVALUE, &variable))
PyObject *default_value = Py_BuildValue( STRINGVALUE, "" ) ;
// If a default value was not provided, see if we were called with
// just the value to find.
if (!PyArg_ParseTuple(args, CC("s|O"), &variable, &default_value))
return NULL;
config const &memory = running_instance->current_team().ai_memory();
char const *s = memory[variable].c_str();
if (s && s[0])
@ -1725,10 +1733,14 @@ PyObject* python_ai::wrapper_get_variable(PyObject* /*self*/, PyObject* args)
}
PyObject *ret = PyMarshal_ReadObjectFromString(data, len / 2);
delete[] data;
return ret;
}
return ret ;
} else if( default_value ) {
// Value did not exist - do return default value
return default_value ;
}
Py_INCREF(Py_None);
return Py_None;
return Py_None;
}
PyObject* python_ai::wrapper_get_version(PyObject* /*self*/, PyObject* args)
@ -1882,10 +1894,12 @@ static PyMethodDef wesnoth_python_methods[] = {
"used to make the AI save strings (and other python values which can "
"be marshalled) over multiple turns.")
MDEF("get_variable", python_ai::wrapper_get_variable,
"Parameters: variable\n"
"Returns: value\n"
"Parameters: variable, default value\n"
"Returns: value or default value\n"
"Retrieves a persistent variable 'variable' from the AI, which has "
"previously been set with set_variable - or None if it can't be found.")
"previously been set with set_variable - or None if it can't be found and\n"
"default value has not been set. If a default value is set and the value"
"can not be found, the default value is returned.")
MDEF("get_version", python_ai::wrapper_get_version,
"Returns a string containing current Wesnoth version")
MDEF("raise_user_interact", python_ai::wrapper_raise_user_interact,
@ -2054,6 +2068,7 @@ void python_ai::play_turn()
// They have to end with .py, and have #!WPY as first line.
std::vector<std::string> python_ai::get_available_scripts()
{
int allow_unsafe = !preferences::run_safe_python() ;
std::vector<std::string> scripts;
const std::vector<std::string>& paths = get_binary_paths("data");
for(std::vector<std::string>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
@ -2068,6 +2083,9 @@ std::vector<std::string> python_ai::get_available_scripts()
if (mark == "#!WPY" &&
std::find(scripts.begin(), scripts.end(), name) == scripts.end())
scripts.push_back(name);
else if (allow_unsafe && mark == "#!UNSAFE_WPY" &&
std::find(scripts.begin(), scripts.end(), name) == scripts.end())
scripts.push_back(name);
}
}
}