|
| 1 | +--- |
| 2 | +description: In this example, we bind a multiple matplotlib Python classes for plotting line charts. |
| 3 | +--- |
| 4 | + |
| 5 | +# Another matplotlib example |
| 6 | + |
| 7 | +Let's take another look at [matplotlib](https://matplotlib.org/). This one will be a little different in that we will generate direct bindings for a couple of matplotlib classes and module functions. |
| 8 | + |
| 9 | +* [Axes](https://matplotlib.org/stable/api/axes_api.html#matplotlib.axes.Axes) |
| 10 | +* [Figure](https://matplotlib.org/stable/api/figure_api.html#module-matplotlib.figure) |
| 11 | +* [matplotlib.pyplot.subplots](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.subplots.html#matplotlib.pyplot.subplots) |
| 12 | + |
| 13 | +This example will also show you some of the current limitations of `pyml_bindgen` :) |
| 14 | + |
| 15 | +## Value specs |
| 16 | + |
| 17 | +For this example, we won't bother binding all the arguments that these methods take since we won't be using them. |
| 18 | + |
| 19 | +For each of these, I will put down the arguments as shown in the matplotlib docs, the follow it with the OCaml value spec we will use. |
| 20 | + |
| 21 | +### `Axes.set_title` |
| 22 | + |
| 23 | +([docs](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.set_title.html#matplotlib.axes.Axes.set_title)) |
| 24 | + |
| 25 | +Python: |
| 26 | + |
| 27 | +``` |
| 28 | +Axes.set_title(label, fontdict=None, loc=None, pad=None, *, y=None, **kwargs) |
| 29 | +``` |
| 30 | + |
| 31 | +OCaml: |
| 32 | + |
| 33 | +```ocaml |
| 34 | +val set_title : t -> label:string -> unit -> unit |
| 35 | +``` |
| 36 | + |
| 37 | +### `Axes.plot` |
| 38 | + |
| 39 | +([docs](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.plot.html#matplotlib.axes.Axes.plot)) |
| 40 | + |
| 41 | +Python: |
| 42 | + |
| 43 | +``` |
| 44 | +Axes.plot(*args, scalex=True, scaley=True, data=None, **kwargs) |
| 45 | +``` |
| 46 | + |
| 47 | +OCaml: |
| 48 | + |
| 49 | +```ocaml |
| 50 | +val plot : t -> x:float list -> y:float list -> ?color:string -> unit -> unit |
| 51 | +``` |
| 52 | + |
| 53 | +This value spec will generate an `Axes.plot` function actually has a bug. If you check the docs, you can't actually pass `x` and `y` as keyword arguments. Oops! You have to go in and edit the binding by hand. Below, I will show a [patch file](TODO) with the changes you need to make. |
| 54 | + |
| 55 | +You may be thinking, well, that's pretty annoying...I agree! For this function, I would probably just write it by hand from the start. I'm showing it here partly as a reminder that I want to change the behaviour of `pyml_bindgen` in a future release to handle methods like this one. But for now, you have to deal with it yourself :) |
| 56 | + |
| 57 | +### `Figure.savefig` |
| 58 | + |
| 59 | +([docs](https://matplotlib.org/stable/api/figure_api.html#matplotlib.figure.Figure.savefig)) |
| 60 | + |
| 61 | +``` |
| 62 | +savefig(fname, *, transparent=None, **kwargs) |
| 63 | +``` |
| 64 | + |
| 65 | + |
| 66 | +```ocaml |
| 67 | +val savefig : t -> fname:string -> unit -> unit |
| 68 | +``` |
| 69 | + |
| 70 | +## Generating `Axes` & `Figure` modules |
| 71 | + |
| 72 | +Let's go over the arguments and options for `pyml_bindgen` that we will need. |
| 73 | + |
| 74 | +Note that we need to specify the correct Python module from which the `Axes` and `Figure` classes come. |
| 75 | + |
| 76 | +* For `Axes`, that's `matplotlib.axes`. |
| 77 | +* For `Figure`, that's `matplotlib.figure`. |
| 78 | + |
| 79 | +We use `--caml-module` option again to tell `pyml_bindgen` to generate the module signature as well as the implementation. |
| 80 | + |
| 81 | +We use `-a class` to specify that we want to generate class-associated methods. Note that this is the default option. |
| 82 | + |
| 83 | +For both invocations, we pipe the output directly to `ocamlformat`. |
| 84 | + |
| 85 | +For now, `pyml_bindgen` always generates the `filter_opt` helper function. If you're generating multiple modules and concatenating them, you'll have to delete the function by hand or with `grep` or something. In our case, I use `grep` to remove the line from the `Figure` generating command. In later versions, you will be able to control this from the command line. |
| 86 | + |
| 87 | +### Run `pyml_bindgen` |
| 88 | + |
| 89 | +Here are the commands. |
| 90 | + |
| 91 | +``` |
| 92 | +pyml_bindgen axes_specs.txt matplotlib.axes Axes --caml-module Axes -a class \ |
| 93 | + | ocamlformat --enable-outside-detected-project --name=a.ml - \ |
| 94 | + > py_class.ml |
| 95 | +
|
| 96 | +printf "\n" >> py_class.ml |
| 97 | +
|
| 98 | +pyml_bindgen figure_specs.txt matplotlib.figure Figure --caml-module Figure -a class \ |
| 99 | + | grep -v 'let filter_opt' \ |
| 100 | + | ocamlformat --enable-outside-detected-project --name=a.ml - \ |
| 101 | + >> py_class.ml |
| 102 | +``` |
| 103 | + |
| 104 | +### Patch the file |
| 105 | + |
| 106 | +Above, I mentioned that you would need to change the implementation for the `Axes` module a bit. Here is the patch for the lines you need to change. |
| 107 | + |
| 108 | +Here is a patch showing the change I mean |
| 109 | + |
| 110 | +``` |
| 111 | +--- py_class_bug.ml 2021-10-20 20:21:00.000000000 -0400 |
| 112 | ++++ py_class.ml 2021-10-20 20:21:00.000000000 -0400 |
| 113 | +@@ -30,17 +30,21 @@ |
| 114 | +
|
| 115 | + let plot t ~x ~y ?color () = |
| 116 | + let callable = Py.Object.find_attr_string t "plot" in |
| 117 | ++ let args = |
| 118 | ++ [| |
| 119 | ++ Py.List.of_list_map Py.Float.of_float x; |
| 120 | ++ Py.List.of_list_map Py.Float.of_float y; |
| 121 | ++ |] |
| 122 | ++ in |
| 123 | + let kwargs = |
| 124 | + filter_opt |
| 125 | + [ |
| 126 | +- Some ("x", Py.List.of_list_map Py.Float.of_float x); |
| 127 | +- Some ("y", Py.List.of_list_map Py.Float.of_float y); |
| 128 | + (match color with |
| 129 | + | Some color -> Some ("color", Py.String.of_string color) |
| 130 | + | None -> None); |
| 131 | + ] |
| 132 | + in |
| 133 | +- ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs |
| 134 | ++ ignore @@ Py.Callable.to_function_with_keywords callable args kwargs |
| 135 | + end |
| 136 | +
|
| 137 | + module Figure : sig |
| 138 | +``` |
| 139 | + |
| 140 | +### Generated output |
| 141 | + |
| 142 | +Here's the whole of the generated output including the patch. |
| 143 | + |
| 144 | +```ocaml |
| 145 | +let filter_opt l = List.filter_map Fun.id l |
| 146 | +
|
| 147 | +module Axes : sig |
| 148 | + type t |
| 149 | +
|
| 150 | + val of_pyobject : Pytypes.pyobject -> t option |
| 151 | +
|
| 152 | + val to_pyobject : t -> Pytypes.pyobject |
| 153 | +
|
| 154 | + val set_title : t -> label:string -> unit -> unit |
| 155 | +
|
| 156 | + val plot : t -> x:float list -> y:float list -> ?color:string -> unit -> unit |
| 157 | +end = struct |
| 158 | + let import_module () = Py.Import.import_module "matplotlib.axes" |
| 159 | +
|
| 160 | + type t = Pytypes.pyobject |
| 161 | +
|
| 162 | + let is_instance pyo = |
| 163 | + let py_class = Py.Module.get (import_module ()) "Axes" in |
| 164 | + Py.Object.is_instance pyo py_class |
| 165 | +
|
| 166 | + let of_pyobject pyo = if is_instance pyo then Some pyo else None |
| 167 | +
|
| 168 | + let to_pyobject x = x |
| 169 | +
|
| 170 | + let set_title t ~label () = |
| 171 | + let callable = Py.Object.find_attr_string t "set_title" in |
| 172 | + let kwargs = filter_opt [ Some ("label", Py.String.of_string label) ] in |
| 173 | + ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs |
| 174 | +
|
| 175 | + let plot t ~x ~y ?color () = |
| 176 | + let callable = Py.Object.find_attr_string t "plot" in |
| 177 | + let args = |
| 178 | + [| |
| 179 | + Py.List.of_list_map Py.Float.of_float x; |
| 180 | + Py.List.of_list_map Py.Float.of_float y; |
| 181 | + |] |
| 182 | + in |
| 183 | + let kwargs = |
| 184 | + filter_opt |
| 185 | + [ |
| 186 | + (match color with |
| 187 | + | Some color -> Some ("color", Py.String.of_string color) |
| 188 | + | None -> None); |
| 189 | + ] |
| 190 | + in |
| 191 | + ignore @@ Py.Callable.to_function_with_keywords callable args kwargs |
| 192 | +end |
| 193 | +
|
| 194 | +module Figure : sig |
| 195 | + type t |
| 196 | +
|
| 197 | + val of_pyobject : Pytypes.pyobject -> t option |
| 198 | +
|
| 199 | + val to_pyobject : t -> Pytypes.pyobject |
| 200 | +
|
| 201 | + val savefig : t -> fname:string -> unit -> unit |
| 202 | +end = struct |
| 203 | + let import_module () = Py.Import.import_module "matplotlib.figure" |
| 204 | +
|
| 205 | + type t = Pytypes.pyobject |
| 206 | +
|
| 207 | + let is_instance pyo = |
| 208 | + let py_class = Py.Module.get (import_module ()) "Figure" in |
| 209 | + Py.Object.is_instance pyo py_class |
| 210 | +
|
| 211 | + let of_pyobject pyo = if is_instance pyo then Some pyo else None |
| 212 | +
|
| 213 | + let to_pyobject x = x |
| 214 | +
|
| 215 | + let savefig t ~fname () = |
| 216 | + let callable = Py.Object.find_attr_string t "savefig" in |
| 217 | + let kwargs = filter_opt [ Some ("fname", Py.String.of_string fname) ] in |
| 218 | + ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs |
| 219 | +end |
| 220 | +``` |
| 221 | + |
| 222 | +## Write the `Pyplot` module |
| 223 | + |
| 224 | +For a little variety, and because we don't need any of the extra stuff that `pyml_bindgen` generates (again, you will be able to control this eventually), let's write this one by hand. |
| 225 | + |
| 226 | +Then you can make a `pyplot.ml` file |
| 227 | + |
| 228 | +```ocaml |
| 229 | +open Py_class |
| 230 | +
|
| 231 | +let import_module () = Py.Import.import_module "matplotlib.pyplot" |
| 232 | +
|
| 233 | +let subplots () = |
| 234 | + let callable = Py.Module.get (import_module ()) "subplots" in |
| 235 | + let args = [||] in |
| 236 | + let kwargs = [] in |
| 237 | + let tup = Py.Callable.to_function_with_keywords callable args kwargs in |
| 238 | + let fig, ax = Py.Tuple.to_tuple2 tup in |
| 239 | + match (Figure.of_pyobject fig, Axes.of_pyobject ax) with |
| 240 | + | Some f, Some a -> Some (f, a) |
| 241 | + | Some _, None | None, Some _ | None, None -> None |
| 242 | +``` |
| 243 | + |
| 244 | +Note that there are more compact ways to write this with `pyml`, but we will leave it like this to keep it similar to the rest of the generated functions. |
| 245 | + |
| 246 | +## Set up the Dune project and run it |
| 247 | + |
| 248 | +Now we need a dune file and a driver to run our plotting code. Save these two files in the same directory in as the other files. |
| 249 | + |
| 250 | +`dune` |
| 251 | + |
| 252 | +``` |
| 253 | +(executable |
| 254 | + (name run) |
| 255 | + (libraries pyml)) |
| 256 | +``` |
| 257 | + |
| 258 | +`run.ml` |
| 259 | + |
| 260 | +```ocaml |
| 261 | +open Py_class |
| 262 | +
|
| 263 | +let () = Py.initialize () |
| 264 | +
|
| 265 | +let figure, axes = |
| 266 | + match Pyplot.subplots () with |
| 267 | + | Some (fig, ax) -> (fig, ax) |
| 268 | + | None -> failwith "Failed to make figure and axes!" |
| 269 | +
|
| 270 | +let x = [ 1.; 2.; 3.; 4.; 5. ] |
| 271 | +let y = [ 1.; 1.5; 2.; 3.; 3.5 ] |
| 272 | +
|
| 273 | +let () = Axes.set_title axes ~label:"Brown Plot" () |
| 274 | +let () = Axes.plot axes ~x ~y ~color:"tab:brown" () |
| 275 | +let () = Figure.savefig figure ~fname:"brown_plot.png" () |
| 276 | +``` |
| 277 | + |
| 278 | +Run it like so: |
| 279 | + |
| 280 | +``` |
| 281 | +$ dune exec ./run.exe |
| 282 | +``` |
| 283 | + |
| 284 | +If all goes well, you should see a nice, brown line plot: |
| 285 | + |
| 286 | + |
| 287 | + |
| 288 | +## Wrap up |
| 289 | + |
| 290 | +In this tutorial, we generating bindings for a couple of matplotlib classes and functions. You saw how to combine multiple generated modules as well as some of the little workarounds you still have to do. |
| 291 | + |
| 292 | +Like all the examples so far, we're only binding a couple of classes & functions. For such a small thing, feel free to write your bindings by hand. These two classes alone have tons of functions though, so if you were binding them all, that would be a pain to write by hand! |
0 commit comments