From 2618cc3d1572afcaa0bfa354dfdaf916d1001466 Mon Sep 17 00:00:00 2001 From: Greg Copeland Date: Mon, 30 Jun 2008 13:44:59 +0000 Subject: [PATCH] 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. --- changelog | 24 ++++++++++++++++++++++-- data/ais/parse.py | 2 +- data/ais/safe.py | 30 ++++++++++++++++++++---------- src/ai_python.cpp | 32 +++++++++++++++++++++++++------- 4 files changed, 68 insertions(+), 20 deletions(-) diff --git a/changelog b/changelog index f9c97b38a8a..098e5e78fb1 100644 --- a/changelog +++ b/changelog @@ -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 diff --git a/data/ais/parse.py b/data/ais/parse.py index 6afb216b1e5..3b09b4e59bb 100644 --- a/data/ais/parse.py +++ b/data/ais/parse.py @@ -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 diff --git a/data/ais/safe.py b/data/ais/safe.py index e506be82024..f683801b7cd 100644 --- a/data/ais/safe.py +++ b/data/ais/safe.py @@ -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 ) diff --git a/src/ai_python.cpp b/src/ai_python.cpp index 109c286b620..70f6ffcfa5f 100644 --- a/src/ai_python.cpp +++ b/src/ai_python.cpp @@ -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 python_ai::get_available_scripts() { + int allow_unsafe = !preferences::run_safe_python() ; std::vector scripts; const std::vector& paths = get_binary_paths("data"); for(std::vector::const_iterator i = paths.begin(); i != paths.end(); ++i) { @@ -2068,6 +2083,9 @@ std::vector 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); } } }