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); } } }