changeset 225:0c6f7ae8a95b

Merged in macdonald/pytave (pull request #6) add @pyobject support
author Mike Miller <mike@mtmxr.com>
date Wed, 20 Jul 2016 11:12:36 -0700
parents 377f2dc057ea (current diff) 7feece80fbfa (diff)
children 382bb1d91239
files Makefile.am octave_to_python.cc pyeval.cc python_to_octave.cc
diffstat 7 files changed, 559 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/@pyobject/display.m	Wed Jul 20 11:12:36 2016 -0700
@@ -0,0 +1,61 @@
+## Copyright (C) 2016 Colin B. Macdonald
+##
+## This file is part of PyTave.
+##
+## OctSymPy is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; either version 3 of the License,
+## or (at your option) any later version.
+##
+## This software is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty
+## of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
+## the GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public
+## License along with this software; see the file COPYING.
+## If not, see <http://www.gnu.org/licenses/>.
+
+## -*- texinfo -*-
+## @documentencoding UTF-8
+## @defmethod @@pyobject display (@var{x})
+## Custom display for pyobjects.
+##
+## Example:
+## @example
+## @group
+## pyexec('import sys')
+## sysmodule = pyeval('sys')
+##   @result{} sysmodule = [pyobject ...]
+##
+##       <module 'sys' (built-in)>
+##
+## @end group
+## @end example
+##
+## @seealso{@@pyobject/disp}
+## @end defmethod
+
+
+function display (x)
+
+  loose = ! __compactformat__ ();
+
+  printf ("%s = [pyobject %s]\n", inputname (1), getid (x));
+  s = disp (x);
+  s = make_indented (s);
+  if (loose), printf("\n"); endif
+  disp (s)
+  if (loose), printf("\n"); endif
+
+endfunction
+
+
+function s = make_indented(s, n)
+  if (nargin == 1)
+    n = 2;
+  endif
+  pad = char (double (" ")*ones (1,n));
+  s = strrep (s, "\n", ["\n" pad]);
+  s = [pad s];  # first line
+endfunction
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/@pyobject/dummy.m	Wed Jul 20 11:12:36 2016 -0700
@@ -0,0 +1,132 @@
+## Copyright (C) 2016 Colin B. Macdonald
+##
+## This file is part of PyTave.
+##
+## OctSymPy is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; either version 3 of the License,
+## or (at your option) any later version.
+##
+## This software is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty
+## of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
+## the GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public
+## License along with this software; see the file COPYING.
+## If not, see <http://www.gnu.org/licenses/>.
+
+## -*- texinfo -*-
+## @documentencoding UTF-8
+## @defmethod @@pyobject dummy (@var{x})
+## Does nothing, stores doctests for now.
+##
+##
+## Simple example:
+## @example
+## @group
+## pyexec ("g = 6")
+## g = pyobject.fromPythonVarName ("g");
+##
+## sort (methods (g))
+##   @result{} ans =
+##     @{
+##       [1,1] = bit_length
+##       [1,2] = conjugate
+##       [1,3] = denominator
+##       [1,4] = imag
+##       [1,5] = numerator
+##       [1,6] = real
+##      @}
+##
+## g.numerator
+##   @result{} ans =  6
+## g.denominator
+##   @result{} ans =  1
+## @end group
+## @end example
+##
+##
+## You can delete an object in Python and it will persist:
+## @example
+## @group
+## pyexec ("d = dict(one=1, two=2)")
+## x = pyobject.fromPythonVarName ("d")
+##   @result{} x = [pyobject ...]
+##       @{'two': 2, 'one': 1@}
+##
+## # oops, overwrote d in Python:
+## pyexec ("d = 42")
+##
+## # but have no fear, we still have a reference to it:
+## x
+##   @result{} x = [pyobject ...]
+##       @{'two': 2, 'one': 1@}
+## @end group
+## @end example
+##
+## We can accesss ``callables'' (methods) of objects:
+## @example
+## @group
+## x.keys()
+##   @result{} ans =
+##       @{
+##         [1,1] = two
+##         [1,2] = one
+##       @}
+## @end group
+## @end example
+##
+## @code{pyeval} returns a @@pyobject for things it cannot convert to
+## Octave-native objects:
+## @example
+## @group
+## pyexec ("import sys")
+## sysmodule = pyeval ("sys")
+##   @result{} sysmodule = [pyobject ...]
+##       <module 'sys' (built-in)>
+## @end group
+## @end example
+##
+## After you have the object, you can access its properties:
+## @example
+## @group
+## sysmodule.version
+##   @result{} ans = ...
+## @end group
+## @end example
+##
+##
+## TODO: this should return a cell array with a double, a string,
+## and an @@pyobject in it:
+## @example
+## @group
+## pyeval ("[42, 'hello', sys]")         # doctest: +XFAIL
+##   @result{} ans =
+##       @{
+##         [1,1] =  42
+##         [1,2] = hello
+##         [1,3] =
+##           [PyObject id ...]
+##           <module 'sys' (built-in)>
+##       @}
+## @end group
+## @end example
+##
+## A @@pyobject can be passed back to Python.  This does not make
+## a copy but rather a reference to the original object.
+## For example:
+## @example
+## @group
+## pycall ("__builtin__.print", sysmodule)   # doctest: +XFAIL
+##   @print{} <module 'sys' (built-in)>
+## @end group
+## @end example
+## (FIXME: I think this failure may correspond to an existing doctest issue).
+##
+## @seealso{pyobject}
+## @end defmethod
+
+function dummy (x)
+
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/@pyobject/methods.m	Wed Jul 20 11:12:36 2016 -0700
@@ -0,0 +1,83 @@
+## Copyright (C) 2016 Colin B. Macdonald
+##
+## This file is part of PyTave.
+##
+## OctSymPy is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; either version 3 of the License,
+## or (at your option) any later version.
+##
+## This software is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty
+## of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
+## the GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public
+## License along with this software; see the file COPYING.
+## If not, see <http://www.gnu.org/licenses/>.
+
+## -*- texinfo -*-
+## @documentencoding UTF-8
+## @defmethod @@pyobject methods (@var{x})
+## List the properties/callables of a Python object.
+##
+## Returns a cell array of strings, the names of the properties
+## and ``callables'' of @var{x}.
+##
+## Example:
+## @example
+## @group
+## pyexec ("import sys")
+## sys = pyeval ("sys");
+## methods (sys)
+##   @result{} ans =
+##     @{
+##       [1,1] = ...
+##       [1,2] = ...
+##        ...  = path
+##        ...  = version
+##        ...
+##     @}
+## @end group
+## @end example
+##
+## Note that if you instead want the methods implemented by
+## the Octave class @code{@@pyobject}, use can always do:
+## @example
+## @group
+## methods pyobject
+##   @print{} Methods for class pyobject:
+##   @print{} display  ...  subsref
+## @comment this doctest may need updating as we add methods
+## @end group
+## @end example
+##
+## @seealso{methods}
+## @end defmethod
+
+
+function L = methods (x)
+  # filter the output of `dir(x)`
+  # (to get properties only:
+  # [a for a in dir(x) if not callable(getattr(x, a)) and not a.startswith('__')]
+  cmd = sprintf ( ...
+    "[a for a in dir(__InOct__['%s']) if not a.startswith('__')]", ...
+    getid(x));
+  # TODO: may need to convert from Python list to Octave list
+  L = pyeval (cmd);
+endfunction
+
+
+%!test
+%! pyexec ("import sys")
+%! sys = pyeval ("sys");
+%! L = methods (sys);
+%! % sys has lots of methods
+%! assert (length (L) >= 32)
+%! % version is one of them
+%! assert (any (strcmp (L, "version")))
+
+%!test
+%! pyexec ("import sys")
+%! L = methods (pyeval ("sys"));
+%! assert (iscell (L))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/@pyobject/pyobject.m	Wed Jul 20 11:12:36 2016 -0700
@@ -0,0 +1,128 @@
+## Copyright (C) 2016 Colin B. Macdonald
+##
+## This file is part of PyTave.
+##
+## OctSymPy is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; either version 3 of the License,
+## or (at your option) any later version.
+##
+## This software is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty
+## of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
+## the GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public
+## License along with this software; see the file COPYING.
+## If not, see <http://www.gnu.org/licenses/>.
+
+## -*- texinfo -*-
+## @documentencoding UTF-8
+## @defun  pyobject (@var{s})
+## Wrap a Python object.
+##
+## TODO: where/how to document classdef classes?
+##
+## @seealso{pyexec, pyeval}
+## @end defun
+
+
+classdef pyobject < handle
+  properties
+    id
+  end
+
+  methods (Static)
+    function x = fromPythonVarName (pyvarname)
+      # Warning: just for testing, may be removed without notice!
+      # If @var{pyvarname} is a string, its assumed to be a variable
+      # name, e.g., previously created with pyexec.  This must exist
+      # at the time of construction but it can disappear later (we
+      # will keep track of the reference).
+      if (! ischar (pyvarname))
+        error("pyobject: currently we only take variable names as input")
+      endif
+      cmd = sprintf ([ ...
+        'if not ("__InOct__" in vars() or "__InOct__" in globals()):\n' ...
+        '    __InOct__ = dict()\n' ...
+        '    # FIXME: make it accessible elsewhere?\n' ...
+        '    import __main__\n' ...
+        '    __main__.__InOct__ = __InOct__\n' ...
+        '__InOct__[hex(id(%s))] = %s' ], ...
+        pyvarname, pyvarname);
+      pyexec (cmd);
+      id = pyeval (["hex(id(" pyvarname "))"]);
+      x = pyobject (id);
+    endfunction
+  endmethods
+
+
+  methods
+    function x = pyobject (id)
+      % warning: not intended for casual use: you must also insert
+      % the object into the Python `__InOct__` dict with key `id`.
+      x.id = id;
+    endfunction
+
+    function delete (x)
+      # Called on clear of the last reference---for subclasses of
+      # handle; not called at all for "value classes".
+      #
+      # FIXME: #46497 this is never called!
+      # Workaround: call @code{delete(x)} right before @code{clear x}.  But
+      # be careful, @code{x} needs to be the last reference: don't do this:
+      # @example
+      # d = pyobject (...);
+      # d2 = d;
+      # delete (d)
+      # clear d
+      # d2
+      #   @print{} ... KeyError ...
+      # @end example
+
+      #disp ("delete")
+
+      # throws KeyError if it wasn't in there for some reason
+      cmd = sprintf ("__InOct__.pop('%s')", x.id);
+      pyexec (cmd)
+    endfunction
+
+    # methods defined in external files
+    dummy (x)
+    display (x)
+    subsref (x, idx)
+
+    function r = getid (x)
+      r = x.id;
+    endfunction
+
+    function varargout = disp (x)
+      s = pyeval (sprintf ("str(__InOct__['%s'])", x.id));
+      if (nargout == 0)
+        disp (s)
+      else
+        varargout = {s};
+      endif
+    endfunction
+
+    function s = whatclass (x)
+      s = pyeval (sprintf ("str(__InOct__['%s'].__class__)", x.id));
+    endfunction
+
+    function vargout = help (x)
+      idx = struct ("type", ".", "subs", "__doc__");
+      s = subsref (x, idx);
+      if (nargout == 0)
+        disp (s)
+      else
+        vargout = {s};
+      endif
+    endfunction
+  endmethods
+endclassdef
+
+
+%!test
+%! pyexec ("import sys")
+%! A = pyeval ("sys");
+%! assert (isa (A, "pyobject"))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/@pyobject/subsref.m	Wed Jul 20 11:12:36 2016 -0700
@@ -0,0 +1,125 @@
+## Copyright (C) 2016 Colin B. Macdonald
+##
+## This file is part of PyTave.
+##
+## OctSymPy is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; either version 3 of the License,
+## or (at your option) any later version.
+##
+## This software is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty
+## of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
+## the GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public
+## License along with this software; see the file COPYING.
+## If not, see <http://www.gnu.org/licenses/>.
+
+## -*- texinfo -*-
+## @documentencoding UTF-8
+## @defop  Method   @@pyobject subsref (@var{x}, @var{idx})
+## @defopx Operator @@pyobject {@var{x}.@var{property}} {}
+## @defopx Operator @@pyobject {@var{x}.@var{method}(@var{a}, @dots)} {}
+## @defopx Operator @@pyobject {@var{x}@{@var{i}@}} {}
+## @defopx Operator @@pyobject {@var{x}@{@var{i}, @var{j}, @dots@}} {}
+## @defopx Operator @@pyobject {@var{x}(@var{a})} {}
+## @defopx Operator @@pyobject {@var{x}(@var{a}, @var{b}, @dots)} {}
+## Call methods and access properties of a Python object.
+##
+##
+## @seealso{@@pyobject/subsasgn}
+## @end defop
+
+
+function r = subsref (x, idx)
+  s = "";
+  for i = 1:length (idx)
+    t = idx(i);
+    switch t.type
+      case "()"
+        if (! isempty (t.subs))
+          t
+          error("not implemented: function calls with arguments")
+        endif
+        s = sprintf ("%s()", s);
+      case "."
+        assert (ischar (t.subs))
+        s = sprintf ("%s.%s", s, t.subs);
+      case "{}"
+        subsstrs = {};
+        for j = 1:length (t.subs)
+	  thissub = t.subs{j};
+          if (ischar (thissub) && strcmp (thissub, ":"))
+            subsstrs{j} = ":";
+          elseif (ischar (thissub))
+            subsstrs{j} = ["'" thissub "'"];
+          elseif (isnumeric (thissub) && isscalar (thissub))
+	    % note: python indexed from 0
+            subsstrs{j} = num2str (thissub - 1);
+          else
+            thissub
+            error ("@pyobject/subsref: subs not supported")
+          endif
+        endfor
+        s = [s "[" strjoin(subsstrs, ", ") "]"];
+      otherwise
+        t
+        error("@pyobject/subsref: not implemented")
+    endswitch
+  endfor
+  r = pyeval (sprintf ("__InOct__['%s']%s", x.id, s));
+endfunction
+
+
+%!test
+%! % list indexing
+%! pyexec ("L = [10, 20]")
+%! L = pyobject.fromPythonVarName ("L");
+%! assert (L{1}, 10)
+%! assert (L{2}, 20)
+
+%!test
+%! % list indexing
+%! pyexec ("L = [10, 20, [30, 40]]")
+%! L = pyobject.fromPythonVarName ("L");
+%! L2 = L{:};
+%! assert (L2{1}, 10)
+%! assert (L2{2}, 20)
+%! assert (L2{3}{1}, 30)
+%! assert (L2{3}{2}, 40)
+
+%!test
+%! % list indexing, nested list
+%! pyexec ("L = [1, 2, [10, 11, 12]]")
+%! L = pyobject.fromPythonVarName ("L");
+%! assert (L{2}, 2)
+%! assert (L{3}{1}, 10)
+%! assert (L{3}{3}, 12)
+
+%!test
+%! % 2D array indexing
+%! pyexec ("import numpy")
+%! pyexec ("A = numpy.array([[1, 2], [3, 4]])")
+%! A = pyobject.fromPythonVarName ("A");
+%! assert (A{1, 1}, 1)
+%! assert (A{2, 1}, 3)
+%! assert (A{1, 2}, 2)
+
+%!test
+%! % dict: str key access
+%! pyexec ("d = {'one':1, 5:5, 6:6}")
+%! d = pyobject.fromPythonVarName ("d");
+%! assert (d{"one"}, 1)
+
+%!test
+%! % dict: integer key access
+%! pyexec ("d = {5:42, 6:42}")
+%! d = pyobject.fromPythonVarName ("d");
+%! assert (d{6}, 42)
+
+%!xtest
+%! % dict: integer key should not subtract one (FIXME: Issue #10)
+%! pyexec ("d = {5:40, 6:42}")
+%! d = pyobject.fromPythonVarName ("d");
+%! assert (d{6}, 42)
--- a/octave_to_python.cc	Fri Jul 15 00:51:23 2016 -0700
+++ b/octave_to_python.cc	Wed Jul 20 11:12:36 2016 -0700
@@ -31,6 +31,7 @@
 #include <octave/oct.h>
 #include <octave/ov.h>
 #include <octave/oct-map.h>
+#include <octave/parse.h>
 
 #include <iostream>
 #include "arrayobjectdefs.h"
@@ -277,7 +278,13 @@
       octvalue_to_pyarr (py_object, octvalue);
     else if (octvalue.is_map ())
       octmap_to_pyobject (py_object, octvalue.map_value ());
-    else
+    else if (octvalue.is_object ()) {
+      octave_value_list tmp = feval ("getid", ovl (octvalue), 1);
+      std::string hexid = tmp(0).string_value();
+      //std::cerr << "passed in hexid: " << hexid << std::endl;
+      // FIXME: added a messy ref to __InOct__ in __main__, find a better way
+      py_object = boost::python::import("__main__").attr("__InOct__")[hexid];
+    } else
       throw value_convert_exception (
         "Conversion from Octave value not implemented");
   }
--- a/pyeval.cc	Fri Jul 15 00:51:23 2016 -0700
+++ b/pyeval.cc	Wed Jul 20 11:12:36 2016 -0700
@@ -29,6 +29,7 @@
 #include <boost/python/numeric.hpp>
 
 #include <oct.h>
+#include <octave/parse.h>
 
 #define PYTAVE_DO_DECLARE_SYMBOL
 #include "arrayobjectdefs.h"
@@ -64,13 +65,21 @@
 
   std::string code = args(0).string_value ();
 
+  std::string id;
+  object res;
+
   Py_Initialize ();
 
+  object main_module = import ("__main__");
+  object main_namespace = main_module.attr ("__dict__");
+
   try
     {
-      object main_module = import ("__main__");
-      object main_namespace = main_module.attr ("__dict__");
-      object res = eval (code.c_str (), main_namespace, main_namespace);
+      res = eval (code.c_str (), main_namespace, main_namespace);
+      object builtins = main_module.attr ("__builtins__");
+      // hex(id(res))
+      object idtmp = builtins.attr("hex")(builtins.attr("id")(res));
+      id = extract<std::string> (idtmp);
 
       // FIXME: currently, we cannot return the raw object to octave...
       if (! res.is_none ())
@@ -82,7 +91,16 @@
     }
   catch (pytave::object_convert_exception const &)
     {
-      error ("pyeval: error in return value type conversion");
+      // Ensure we have a __InOct__ dict, and then put `res` into it
+      exec ("if not (\"__InOct__\" in vars() or \"__InOct__\" in globals()):\n"
+            "    __InOct__ = dict()\n"
+            "    # FIXME: make it accessible elsewhere?\n"
+            "    import __main__\n"
+            "    __main__.__InOct__ = __InOct__\n",
+            main_namespace, main_namespace);
+      main_namespace["__InOct__"][id] = res;
+      // Create @pyobject
+      retval = feval ("pyobject", ovl (id), 1);
     }
   catch (error_already_set const &)
     {