├── .gitattributes ├── .github └── workflows │ ├── build_and_test.yml │ └── generate_docs.yml ├── .gitignore ├── .ocamlformat ├── CHANGELOG.md ├── LICENSE-APACHE.txt ├── LICENSE-MIT.txt ├── Makefile ├── README.md ├── _docs_src ├── custom_theme │ └── main.html ├── make_docs.sh ├── mkdocs.yml └── src │ ├── attributes.md │ ├── auto-gen-bindings.md │ ├── cli-options.md │ ├── dictionaries-2.md │ ├── dictionaries.md │ ├── embedding-python.md │ ├── examples.md │ ├── getting-started.md │ ├── gotchas-bugs.md │ ├── images │ └── basic_tutorial_gitk_example.png │ ├── img │ ├── brown_plot.png │ ├── orange_plot.png │ └── silly_plot.png │ ├── index.md │ ├── instance-methods.md │ ├── matplotlib-2.md │ ├── matplotlib.md │ ├── names.md │ ├── ocaml-attributes.md │ ├── of-pyobject.md │ ├── recursive.md │ ├── robots.txt │ ├── rules-overview.md │ ├── split-ml-mli.md │ ├── static-methods.md │ ├── todo.md │ ├── tuples.md │ └── types.md ├── bench ├── adder.py ├── bench_import_module.ml ├── dune └── val_specs.txt ├── bin ├── bin_utils.ml ├── combine_rec_modules.ml ├── dune ├── gen_multi.ml ├── main_cli.ml ├── main_cli.mli └── pyml_bindgen.ml ├── dune-project ├── examples ├── README.md ├── attributes │ ├── README.md │ ├── dune │ ├── dune-project │ ├── lib │ │ ├── dune │ │ ├── examples_attributes_lib.ml │ │ └── specs │ │ │ └── cat.txt │ ├── py │ │ └── cat.py │ ├── run.ml │ └── test │ │ ├── dune │ │ └── run.t ├── embedding_python_source │ ├── README.md │ ├── dune │ ├── dune-project │ ├── lib │ │ ├── dune │ │ ├── math.ml │ │ └── specs │ │ │ └── adder.txt │ ├── py │ │ └── math │ │ │ └── adder.py │ ├── run.ml │ └── test │ │ ├── dune │ │ └── run.t ├── gen_ml_and_mli │ ├── .ocamlformat │ ├── README.md │ ├── bin │ │ ├── dune │ │ └── hello.ml │ ├── dune-project │ ├── lib │ │ ├── dune │ │ ├── orange.ml │ │ ├── orange.mli │ │ ├── py │ │ │ ├── orange.py │ │ │ └── thing.py │ │ ├── specs │ │ │ ├── orange.txt │ │ │ └── thing.txt │ │ ├── thing.ml │ │ └── thing.mli │ └── test │ │ ├── dune │ │ └── it_works.t ├── importing_modules │ ├── README.md │ ├── dune │ ├── dune-project │ ├── lib │ │ ├── dune │ │ ├── magic_dust.ml │ │ ├── silly_math.ml │ │ └── specs │ │ │ ├── magic_dust │ │ │ ├── hearts.txt │ │ │ └── sparkles.txt │ │ │ └── silly_math │ │ │ ├── adder │ │ │ └── add.txt │ │ │ └── subtracter │ │ │ └── subtract.txt │ ├── py │ │ ├── magic_dust │ │ │ ├── hearts.py │ │ │ └── sparkles.py │ │ └── silly_math │ │ │ ├── adder │ │ │ └── add.py │ │ │ └── subtracter │ │ │ └── subtract.py │ ├── run.ml │ └── test │ │ ├── dune │ │ └── run.t ├── quick_start │ ├── .ocamlformat │ ├── README.md │ ├── adder.py │ ├── dune │ ├── dune-project │ ├── lib.ml │ ├── run.ml │ ├── test │ │ ├── dune │ │ └── run.t │ └── val_specs.txt ├── recursive_modules │ ├── .ocamlformat │ ├── README.md │ ├── dune │ ├── dune-project │ ├── lib │ │ ├── dune │ │ ├── lib.ml │ │ └── specs │ │ │ ├── cat_spec.txt │ │ │ ├── cli_specs.tsv │ │ │ └── human_spec.txt │ ├── py │ │ ├── cat.py │ │ └── human.py │ ├── run.ml │ └── test │ │ ├── dune │ │ └── run.t └── tuples │ ├── .ocamlformat │ ├── README.md │ ├── dune │ ├── dune-project │ ├── lib.ml │ ├── run.ml │ ├── test │ ├── dune │ └── run.t │ ├── tuples.py │ └── val_specs.txt ├── lib ├── dune ├── mkdir.ml ├── oarg.ml ├── otype.ml ├── py_fun.ml ├── shared.ml ├── specs_file.ml ├── utils.ml └── version.ml ├── pyml_bindgen-dev.opam ├── pyml_bindgen.opam ├── pyml_bindgen.opam.template ├── test ├── README.md ├── basic_class_binding.t │ ├── dune │ ├── dune-project │ ├── run.t │ ├── run_no_check.ml │ ├── run_option.ml │ ├── run_or_error.ml │ ├── sigs_no_check.txt │ ├── sigs_option.txt │ ├── sigs_or_error.txt │ └── silly.py ├── binding_tuples.t │ ├── dune │ ├── lib_ml.txt │ ├── run.ml │ ├── run.t │ ├── sigs.txt │ └── silly.py ├── combine_rec_modules.t │ ├── abc.ml │ ├── d.ml │ ├── e.ml │ ├── no_modules.ml │ ├── no_sig.ml │ ├── one_line.ml │ └── run.t ├── dune ├── embed_source.t │ ├── .ocamlformat │ ├── bad_thing.py │ ├── dune │ ├── dune-project │ ├── person.py │ ├── person2_runner.ml │ ├── person_runner.ml │ ├── person_specs.txt │ ├── person_thing_runner.ml │ ├── py │ │ └── cool_package │ │ │ └── person2.py │ ├── run.t │ ├── thing.py │ └── thing_specs.txt ├── error_messages.t │ ├── .ocamlformat │ ├── run.t │ └── val_specs.txt ├── gen_multi.t │ ├── .ocamlformat │ ├── bad.tsv │ ├── run.t │ └── specs │ │ ├── cat_spec.txt │ │ ├── cli_specs.tsv │ │ ├── cli_specs_cat_first.tsv │ │ └── human_spec.txt ├── helpers │ └── sanitize_logs ├── module_function_binding.t │ ├── dune │ ├── dune-project │ ├── run.t │ ├── run_no_caml_module.ml │ ├── run_with_caml_module.ml │ ├── sigs_no_check.txt │ └── silly.py ├── nested_submodules.t │ ├── creature.ml │ ├── dune │ ├── run.t │ ├── run_no_check.ml │ ├── run_option.ml │ ├── run_or_error.ml │ ├── silly.py │ ├── specs_no_check.txt │ ├── specs_option.txt │ └── specs_or_error.txt ├── of_pyo_no_check.t │ ├── dune │ ├── hi.ml │ ├── run.t │ ├── silly_mod.py │ ├── silly_sigs_no_check.txt │ ├── silly_sigs_no_check_option.txt │ ├── silly_sigs_no_check_or_error.txt │ ├── silly_sigs_option.txt │ ├── silly_sigs_option_or_error.txt │ └── silly_sigs_or_error.txt ├── of_pyo_option.t │ ├── dune │ ├── hi.ml │ ├── run.t │ ├── silly_mod.py │ ├── silly_sigs_no_check.txt │ ├── silly_sigs_no_check_option.txt │ ├── silly_sigs_no_check_or_error.txt │ ├── silly_sigs_option.txt │ ├── silly_sigs_option_or_error.txt │ └── silly_sigs_or_error.txt ├── of_pyo_or_error.t │ ├── dune │ ├── hi.ml │ ├── run.t │ ├── silly_mod.py │ ├── silly_sigs_no_check.txt │ ├── silly_sigs_no_check_option.txt │ ├── silly_sigs_no_check_or_error.txt │ ├── silly_sigs_option.txt │ ├── silly_sigs_option_or_error.txt │ └── silly_sigs_or_error.txt ├── old_bugs.t │ ├── pr_value_bug.txt │ └── run.t ├── py_fun_name_attr.t │ ├── dune │ ├── run.ml │ ├── run.t │ ├── silly.py │ └── specs.txt ├── raw_pyobjects.t │ ├── creature.ml │ ├── dune │ ├── run.ml │ ├── run.t │ ├── silly.py │ └── specs.txt ├── split_caml_module.t │ ├── .ocamlformat │ ├── dune │ ├── dune-project │ ├── hello.ml │ ├── run.t │ ├── specs.txt │ └── thing.py ├── test_oarg.ml ├── test_otype.ml ├── test_py_fun.ml └── test_specs_file.ml └── test_dev ├── README.md ├── combine_rec_modules_cli_error.t ├── dune ├── gen_multi_cli_error.t ├── bad.tsv ├── run.t └── specs │ ├── bad │ └── bad.tsv │ └── bad_file_names.tsv └── pyml_bindgen_cli_error.t ├── run.t └── specs.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | _docs_src -linguist-detectable 2 | test/*/run.t -linguist-detectable -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | - "staging" 8 | - "dev" 9 | 10 | pull_request: 11 | branches: 12 | - "*" 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: 20 | - ubuntu-latest 21 | ocaml-compiler: 22 | - 4.14.0 23 | - 4.08.1 24 | 25 | runs-on: ${{ matrix.os }} 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v2 30 | 31 | - name: Use OCaml ${{ matrix.ocaml-compiler }} 32 | uses: ocaml/setup-ocaml@v2 33 | with: 34 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 35 | dune-cache: true 36 | cache-prefix: v1-${{ matrix.os }} 37 | opam-local-packages: pyml_bindgen.opam 38 | 39 | - run: opam exec -- make deps 40 | 41 | - name: Check release profile 42 | run: | 43 | opam exec -- make clean 44 | opam exec -- make build_release 45 | opam exec -- make test_release 46 | opam exec -- make install_release 47 | 48 | - name: Check dev profile 49 | run: | 50 | opam exec -- make deps_dev 51 | opam exec -- make clean 52 | opam exec -- make build_dev 53 | opam exec -- make test_dev 54 | opam exec -- make install_dev 55 | if: matrix.ocaml-compiler == '4.14.0' 56 | 57 | # Send the coverage. 58 | - name: Send coverage 59 | run: | 60 | opam install bisect_ppx 61 | opam exec -- make send_coverage 62 | if: matrix.ocaml-compiler == '4.14.0' 63 | env: 64 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | -------------------------------------------------------------------------------- /.github/workflows/generate_docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | submodules: recursive 18 | 19 | - run: pip install mkdocs 20 | - run: cd _docs_src && ./make_docs.sh 21 | - run: git checkout gh-pages 22 | - run: if [ -d docs ]; then rm -r docs; fi 23 | - run: mv _docs docs 24 | 25 | - name: Commit site changes 26 | run: | 27 | git config --global user.name github-actions 28 | git config --global user.email github-actions@github.com 29 | git add docs 30 | git commit -m "Update docs site" 31 | git push 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all dirs starting with _ 2 | _* 3 | 4 | # Un-ignore some of them 5 | !_docs_src 6 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | profile = default 2 | version = 0.23.0 3 | parse-docstrings = true 4 | wrap-comments = true 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | ## 0.4.1 (2022-08-01) 4 | 5 | ### Fixed 6 | 7 | - Fixed cram dependency specification 8 | 9 | ## 0.4.0 (2022-07-31) 10 | 11 | ### Added 12 | 13 | - Add helper scripts for binding cyclic Python classes 14 | - `gen_multi` is a wrapper for `pyml_bindgen` that takes a TSV file of command line specs, and runs `pyml_bindgen` on each of them. 15 | - `combine_rec_modules` is a small program that takes generated OCaml modules and "converts" them into recursive modules 16 | - You can combine these two to make it easier to generate recursive modules, which can be useful for binding Python classes that reference each other. 17 | - Add support for `py_arg_name` attribute. It lets you use different argument names in the OCaml bindings than used in the Python functions. 18 | - Add `--split-caml-module` option to split generated module into separate `ml` and `mli` files. 19 | - Add `pyml_bindgen-dev.opam` for easier installation of development dependencies. 20 | 21 | ## 0.3.1 (2022-03-23) 22 | 23 | ### Changed 24 | 25 | - Reduce number of dependencies (including when installing from the GitHub repository). 26 | - Use `re` instead of `re2` for regular expressions. 27 | - Drop some of the dev dependencies from the `opam` file. 28 | 29 | ## 0.3.0 (2022-03-18) 30 | 31 | ### Added 32 | 33 | - Allow nested module types in val specs (e.g., `Food.Dessert.Apple_pie.t`) 34 | - Allow using `Pytypes.pyobject` and `Py.Object.t` in val specs 35 | - Better error messages when parser or `py_fun` creation fails 36 | - You can now use attributes on value specifications. 37 | - Currently the only one available is `py_fun_name`. 38 | - It allows you to decouple the Python method name and the generated OCaml function name. 39 | - See the examples directory on GitHub for more info. 40 | - You can now bind tuples with 2, 3, 4, or 5 elements. 41 | - They can be passed in as arguments, or returned from functions. 42 | - Only basic types and Python objects are allowed in tuples. 43 | - You can also put tuples inside of collections, e.g., `(int * string) list`, but not Options or Or_errors. 44 | 45 | ### Changed 46 | 47 | - Updated docs 48 | - Update to dune 3 49 | - Update to cmdliner 1.1 50 | 51 | ### Fixed 52 | 53 | - Fix some small `otype` bugs 54 | 55 | ## 0.2.0 (2022-02-02) 56 | 57 | - Allow embedding Python source directly into generated OCaml module with the `--embed-python-source` CLI option. See this [issue](https://github.com/mooreryan/ocaml_python_bindgen/issues/5) for more info. 58 | - Fix bug in val spec parsing 59 | - Update docs 60 | - Add full examples in the `examples` directory 61 | 62 | ## 0.1.2 (2021-12-07) 63 | 64 | - Use specific `ocamlformat` version for the tests. See this Opam repository [pull request](https://github.com/ocaml/opam-repository/pull/20162#issuecomment-987010684) for more info. 65 | 66 | ## 0.1.1 (2021-11-04) 67 | 68 | - Update lower bounds for dependencies 69 | - Fix tests to work with BusyBox/Alpine `grep` command 70 | 71 | ## 0.1.0 (2021-10-31) 72 | 73 | Initial release! 74 | -------------------------------------------------------------------------------- /LICENSE-MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ryan M. Moore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BROWSER = firefox 2 | TEST_COV_D = /tmp/pyml_bindgen 3 | 4 | .PHONY: all_dev 5 | all_dev: clean build_dev test_dev 6 | 7 | .PHONY: all_release 8 | all_release: clean build_release test_release 9 | 10 | .PHONY: build_dev 11 | build_dev: 12 | dune build --profile=dev 13 | 14 | .PHONY: build_release 15 | build_release: 16 | dune build --profile=release 17 | 18 | .PHONY: check 19 | check: 20 | dune build @check 21 | 22 | .PHONY: clean 23 | clean: 24 | dune clean 25 | 26 | .PHONY: deps 27 | deps: 28 | opam install ./pyml_bindgen.opam --deps-only --with-doc --with-test 29 | 30 | .PHONY: deps_dev 31 | deps_dev: 32 | opam install ./pyml_bindgen-dev.opam --deps-only 33 | 34 | .PHONY: install_dev 35 | install_dev: 36 | dune install --profile=dev 37 | 38 | .PHONY: install_release 39 | install_release: 40 | dune install --profile=release 41 | 42 | .PHONY: opam_install 43 | opam_install: 44 | opam install ./pyml_bindgen.opam 45 | 46 | .PHONY: promote 47 | promote: 48 | dune promote 49 | 50 | .PHONY: uninstall 51 | uninstall: 52 | dune uninstall 53 | 54 | .PHONY: test_dev 55 | test_dev: 56 | dune runtest --profile=dev 57 | 58 | .PHONY: test_release 59 | test_release: 60 | dune runtest --profile=release 61 | 62 | .PHONY: test_coverage 63 | test_coverage: 64 | if [ -d $(TEST_COV_D) ]; then rm -r $(TEST_COV_D); fi 65 | mkdir -p $(TEST_COV_D) 66 | BISECT_FILE=$(TEST_COV_D)/pyml_bindgen dune runtest --no-print-directory \ 67 | --instrument-with bisect_ppx --force 68 | bisect-ppx-report html --coverage-path $(TEST_COV_D) 69 | bisect-ppx-report summary --coverage-path $(TEST_COV_D) 70 | 71 | .PHONY: test_coverage_open 72 | test_coverage_open: test_coverage 73 | $(BROWSER) _coverage/index.html 74 | 75 | .PHONY: send_coverage 76 | send_coverage: test_coverage 77 | bisect-ppx-report send-to Coveralls --coverage-path $(TEST_COV_D) 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OCaml-Python Bindings Generator 2 | 3 | [![Build and test](https://github.com/mooreryan/ocaml_python_bindgen/actions/workflows/build_and_test.yml/badge.svg?branch=main)](https://github.com/mooreryan/ocaml_python_bindgen/actions/workflows/build_and_test.yml) [![Coverage Status](https://coveralls.io/repos/github/mooreryan/ocaml_python_bindgen/badge.svg?branch=main)](https://coveralls.io/github/mooreryan/ocaml_python_bindgen?branch=main) 4 | 5 | Generate Python bindings with [pyml](https://github.com/thierry-martinez/pyml) directly from OCaml value specifications. 6 | 7 | While you _could_ write all your Python bindings by hand, it can be tedious and it gets old real quick. While `pyml_bindgen` can't yet auto-generate all the bindings you may need, it can definitely take care of a lot of the tedious and repetitive work you need to do when writing bindings for a big Python library!! 💖 8 | 9 | ## Quick start 10 | 11 | First, install `pyml_bindgen`. It is available on [Opam](https://opam.ocaml.org/packages/pyml_bindgen/). 12 | 13 | ``` 14 | $ opam install pyml_bindgen 15 | ``` 16 | 17 | Say you have a Python class you want to bind and use in OCaml. (Filename: `adder.py`) 18 | 19 | ```python 20 | class Adder: 21 | @staticmethod 22 | def add(x, y): 23 | return x + y 24 | ``` 25 | 26 | To do so, you write OCaml value specifications for the class and methods you want to bind. (Filename: `val_specs.txt`) 27 | 28 | ```ocaml 29 | val add : x:int -> y:int -> unit -> int 30 | ``` 31 | 32 | Then, you run `pyml_bindgen`. 33 | 34 | ``` 35 | $ pyml_bindgen val_specs.txt adder Adder --caml-module Adder > lib.ml 36 | ``` 37 | 38 | Now you can use your generated functions in your OCaml code. (Filename: `run.ml`) 39 | 40 | ```ocaml 41 | open Lib 42 | 43 | let () = Py.initialize () 44 | 45 | let result = Adder.add ~x:1 ~y:2 () 46 | 47 | let () = assert (result = 3) 48 | ``` 49 | 50 | Finally, set up a dune file and run it. 51 | 52 | ``` 53 | (executable 54 | (name run) 55 | (libraries pyml)) 56 | ``` 57 | 58 | ``` 59 | $ dune exec ./run.exe 60 | ``` 61 | 62 | ## Documentation 63 | 64 | For information on installing and using `pyml_bindgen`, check out the [docs](https://mooreryan.github.io/ocaml_python_bindgen/). 65 | 66 | Additionally, you can find examples in the [examples](https://github.com/mooreryan/ocaml_python_bindgen/tree/main/examples) directory. One neat thing about these examples is that you can see how to write Dune [rules](https://dune.readthedocs.io/en/stable/dune-files.html#rule) to automatically generate your `pyml` bindings. 67 | 68 | You may also want to check out my [blog post](https://www.tenderisthebyte.com/blog/2022/04/12/ocaml-python-bindgen/) introducing `pyml_bindgen`. 69 | 70 | ## Installing from sources 71 | 72 | If you want to install from sources, e.g., to track the main branch or a development branch, but you do not want to install all the test and development packages, clone the repository, checkout the branch you want to follow and install: 73 | 74 | ``` 75 | $ git clone https://github.com/mooreryan/ocaml_python_bindgen.git 76 | # Checkout whatever branch you want, in this case `dev`. 77 | $ git checkout dev 78 | $ make opam_install 79 | ``` 80 | 81 | This will save a lot of install time as it avoids some heavy packages. 82 | 83 | ## Development 84 | 85 | If instead, you want to work on `pyml_bindgen` development, will need to ensure you have the development and test dependencies. 86 | 87 | E.g., 88 | 89 | ``` 90 | $ git clone https://github.com/mooreryan/ocaml_python_bindgen.git 91 | $ make deps 92 | $ make deps_dev 93 | # Start working! 94 | $ dune test 95 | ``` 96 | 97 | ## License 98 | 99 | [![license MIT or Apache 100 | 2.0](https://img.shields.io/badge/license-MIT%20or%20Apache%202.0-blue)](https://github.com/mooreryan/pasv) 101 | 102 | Copyright (c) 2021 - 2022 Ryan M. Moore. 103 | 104 | Licensed under the Apache License, Version 2.0 or the MIT license, at your option. This program may not be copied, modified, or distributed except according to those terms. 105 | -------------------------------------------------------------------------------- /_docs_src/custom_theme/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extrahead %} 4 | {% set title = config.site_name %} 5 | 6 | {% if page and page.title and not page.is_homepage %} 7 | {% set title = page.title ~ " - " ~ config.site_name | striptags %} 8 | {% endif %} 9 | 10 | {% set description = config.site_description %} 11 | 12 | {% if page and page.meta.description %} 13 | {% set description = page.meta.description %} 14 | {% endif %} 15 | 16 | {% if page and page.canonical_url %} 17 | {% set canonical_url = page.canonical_url %} 18 | {% endif %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {% if page and page.meta.image %} 39 | {% set image = config.site_url ~ page.meta.image %} 40 | 41 | 42 | {% endif %} 43 | 44 | {% if page and page.meta and page.meta.main %} 45 | 46 | {% endif%} 47 | 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /_docs_src/make_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | time mkdocs build 4 | -------------------------------------------------------------------------------- /_docs_src/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: pyml_bindgen -- OCaml-Python Bindings Generator 2 | site_url: https://mooreryan.github.io/ocaml_python_bindgen/ 3 | repo_url: https://github.com/mooreryan/ocaml_python_bindgen 4 | edit_uri: edit/main/_docs_src/src 5 | site_description: Installation and usage instructions for pyml_bindgen, a program to generate OCaml bindings for Python classes via pyml. 6 | site_author: Ryan M. Moore 7 | nav: 8 | - pyml_bindgen: index.md 9 | - Examples & Tutorials: 10 | - Getting Started: getting-started.md 11 | - Lots of examples: examples.md 12 | - Matplotlib Example: matplotlib.md 13 | - Matplotlib Example 2: matplotlib-2.md 14 | - Handling Dictionaries: dictionaries.md 15 | - Handling Dictionaries 2: dictionaries-2.md 16 | - Recursive Classes: recursive.md 17 | - Rules: 18 | - Overview: rules-overview.md 19 | - Types: types.md 20 | - Function & Argument Names: names.md 21 | - Attributes & Properties: attributes.md 22 | - Instance Methods: instance-methods.md 23 | - Class & Static Methods; Functions: static-methods.md 24 | - Miscellaneous: 25 | - Auto-generating bindings: auto-gen-bindings.md 26 | - Attributes -- py_fun_name: ocaml-attributes.md 27 | - Converting `pyobjects` to OCaml types: of-pyobject.md 28 | - Embedding Python source: embedding-python.md 29 | - Handling Tuples: tuples.md 30 | - Gotchas & Known Bugs: gotchas-bugs.md 31 | - Splitting ml and mli files: split-ml-mli.md 32 | theme: 33 | name: readthedocs 34 | custom_dir: custom_theme 35 | highlightjs: true 36 | hljs_languages: 37 | - ocaml 38 | - python 39 | markdown_extensions: 40 | - meta 41 | docs_dir: src 42 | site_dir: ../_docs 43 | -------------------------------------------------------------------------------- /_docs_src/src/attributes.md: -------------------------------------------------------------------------------- 1 | # Attributes & Properties 2 | 3 | Value specifications that take a single argument `t` will be interpreted as bindings to Python attributes or properties. 4 | 5 | Value specs for attributes and properties look like this: 6 | 7 | ```ocaml 8 | val f : t -> 'a 9 | ``` 10 | 11 | ## Rules 12 | 13 | * The first and only function argument must be `t`. 14 | * The return type can be any of the [types](./types.md) mentioned earlier. 15 | 16 | ## Examples 17 | 18 | ```ocaml 19 | val x : t -> int 20 | val name : t -> string 21 | val price : t -> float 22 | ``` 23 | -------------------------------------------------------------------------------- /_docs_src/src/auto-gen-bindings.md: -------------------------------------------------------------------------------- 1 | # Automatically Generating Bindings 2 | 3 | Sometimes it can be a bit of a pain to always have to run `pyml_bindgen` by hand. 4 | 5 | One way to avoid this is by adding a [rule](https://dune.readthedocs.io/en/stable/dune-files.html#rule) to your Dune file. 6 | 7 | For an example of this, check out the `dune` file in the [quick start](https://github.com/mooreryan/ocaml_python_bindgen/tree/main/examples/quick_start) example on GitHub. It has lots of comments to show you how it is working. 8 | -------------------------------------------------------------------------------- /_docs_src/src/cli-options.md: -------------------------------------------------------------------------------- 1 | # CLI Options 2 | 3 | For reference, here are the CLI opts: 4 | 5 | ```text 6 | $ pyml_bindgen --help 7 | NAME 8 | pyml_bindgen - generate pyml bindings for a set of signatures 9 | 10 | SYNOPSIS 11 | pyml_bindgen [OPTION]... SIGNATURES PY_MODULE PY_CLASS 12 | 13 | DESCRIPTION 14 | Generate pyml bindings from OCaml signatures. 15 | 16 | ARGUMENTS 17 | PY_CLASS (required) 18 | Python class name 19 | 20 | PY_MODULE (required) 21 | Python module name 22 | 23 | SIGNATURES (required) 24 | Path to signatures 25 | 26 | OPTIONS 27 | -a ASSOCIATED_WITH, --associated-with=ASSOCIATED_WITH (absent=class) 28 | Are the Python functions associated with a class or just a module? 29 | ASSOCIATED_WITH must be either `class' or `module'. 30 | 31 | -c CAML_MODULE, --caml-module=CAML_MODULE 32 | Write full module and signature 33 | 34 | --help[=FMT] (default=auto) 35 | Show this help in format FMT. The value FMT must be one of `auto', 36 | `pager', `groff' or `plain'. With `auto', the format is `pager` or 37 | `plain' whenever the TERM env var is `dumb' or undefined. 38 | 39 | -r OF_PYO_RET_TYPE, --of-pyo-ret-type=OF_PYO_RET_TYPE (absent=option) 40 | Return type of the of_pyobject function. OF_PYO_RET_TYPE must be 41 | one of `no_check', `option' or `or_error'. 42 | 43 | --version 44 | Show version information. 45 | 46 | BUGS 47 | Please report any bugs or issues on GitHub. 48 | (https://github.com/mooreryan/pyml_bindgen/issues) 49 | 50 | SEE ALSO 51 | For full documentation, please see the GitHub page. 52 | (https://github.com/mooreryan/pyml_bindgen) 53 | 54 | AUTHORS 55 | Ryan M. Moore 56 | 57 | ``` 58 | -------------------------------------------------------------------------------- /_docs_src/src/embedding-python.md: -------------------------------------------------------------------------------- 1 | # Embedding Python Source Code 2 | 3 | If you import modules in Python code, Python needs to be able to actually find these modules. 4 | 5 | One way is to "install" the Python module you want with say, pip. E.g., `pip install whatever`. 6 | 7 | If you're working on a Python module along with your OCaml code, you probably don't want to do this. Rather, you probably want to set the `PYTHONPATH` environment variable to the location of your Python modules. (You can find an example an detailed explanation of that in the [importing modules](https://github.com/mooreryan/ocaml_python_bindgen/tree/main/examples/importing_modules) example on GitHub.) 8 | 9 | The third option is to embed the Python code directly into the generated OCaml module. The `--embed-python-source` CLI option lets you do this. Basically, you just provide the path to the Python module you want to embed to that option, and it will all work out for you. This way you don't have to have your user's worry about setting the `PYTHONPATH` properly. 10 | 11 | To see it in action, check out the [example](https://github.com/mooreryan/ocaml_python_bindgen/tree/main/examples/embedding_python_source) on GitHub. 12 | 13 | 14 | -------------------------------------------------------------------------------- /_docs_src/src/examples.md: -------------------------------------------------------------------------------- 1 | # Lots of Examples 2 | 3 | You can find a lot of examples on [GitHub](https://github.com/mooreryan/ocaml_python_bindgen/tree/main/examples). 4 | 5 | These examples are particularly cool because they show you how to use Dune [rules](TODO) to generate bindings automatically. Also, they are tested along with the rest of the `pyml_bindgen` code, so they are guaranteed to be up-to-date and working. 6 | -------------------------------------------------------------------------------- /_docs_src/src/gotchas-bugs.md: -------------------------------------------------------------------------------- 1 | # Gotchas & Known Bugs 2 | 3 | * You cannot bind Python properties or attributes that return `None`. So, `val f : t -> unit` will currently fail. 4 | -------------------------------------------------------------------------------- /_docs_src/src/images/basic_tutorial_gitk_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooreryan/ocaml_python_bindgen/e17762a03d8726ecdb92f3e31ae9f6ab3687e8c5/_docs_src/src/images/basic_tutorial_gitk_example.png -------------------------------------------------------------------------------- /_docs_src/src/img/brown_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooreryan/ocaml_python_bindgen/e17762a03d8726ecdb92f3e31ae9f6ab3687e8c5/_docs_src/src/img/brown_plot.png -------------------------------------------------------------------------------- /_docs_src/src/img/orange_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooreryan/ocaml_python_bindgen/e17762a03d8726ecdb92f3e31ae9f6ab3687e8c5/_docs_src/src/img/orange_plot.png -------------------------------------------------------------------------------- /_docs_src/src/img/silly_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooreryan/ocaml_python_bindgen/e17762a03d8726ecdb92f3e31ae9f6ab3687e8c5/_docs_src/src/img/silly_plot.png -------------------------------------------------------------------------------- /_docs_src/src/instance-methods.md: -------------------------------------------------------------------------------- 1 | # Instance Methods 2 | 3 | Value specs for instance methods look like this: 4 | 5 | ```ocaml 6 | val f : t -> a:'a -> ?b:'b -> ... -> unit -> 'c 7 | ``` 8 | 9 | ## Rules 10 | 11 | * The first argument must be `t`. 12 | * The final function argument (penultimate type expression) must be `unit`. 13 | * The return type can be any of the [types](./types.md) mentioned earlier. 14 | * The remaining function arguments must either be named or optional. The types of these arguments can be any of the types mentioned earlier. 15 | 16 | Note on the final unit argument...I require all arguments that bind to Python method arguments be named or optional. Python will often have optional named arguments at the end of a function's arguments. In OCaml, these can't be erased unless you have a unit argument that comes after. So, to keep the APIs all looking similar, I decided that all instance and static methods would end in a final unit argument. This may change in the future. 17 | -------------------------------------------------------------------------------- /_docs_src/src/names.md: -------------------------------------------------------------------------------- 1 | # Function & Argument Names 2 | 3 | You can't pick just any old name for your functions and arguments :) 4 | 5 | The main thing to remember is in addition to being valid OCaml names, they must also be [valid python names](https://docs.python.org/3/reference/lexical_analysis.html#identifiers). This is because we pass the function name and argument names "as-is" to Python. 6 | 7 | 8 | In addition to that, there are a couple other things to keep in mind. 9 | 10 | * Argument names that match any of the [types](types.md) mentioned previously are not allowed. 11 | * Argument names that start with any of the types mentioned are not allowed. (E.g., `val foo : t -> int_thing:string -> unit -> float` will fail.) 12 | * Argument names that end with any of the above types are actually okay. You probably shouldn't name them like this but it works. Really, it's just an artifact of the parsing :) 13 | * Function names and arguments can start with underscores (e.g., `__init__`) but they cannot be *all* underscores. E.g., `val ____ : ...` will not parse. 14 | 15 | ## Reserved keywords 16 | 17 | Sometimes, you are binding a Python method that uses reserved OCaml identifiers either in the name of the method names of the arguments. 18 | 19 | You can get around this using [attributes](./ocaml-attributes.md) in your value specifications. 20 | 21 | ```ocaml 22 | val downto_ : from:int -> to_:int -> unit -> int array 23 | [@@py_fun_name downto] 24 | [@@py_arg_name to_ to] 25 | [@@py_arg_name from from_] 26 | ``` 27 | 28 | (Here is the Python you're binding in this case.) 29 | 30 | ```python 31 | def downto(from_, to): 32 | ... 33 | ``` 34 | -------------------------------------------------------------------------------- /_docs_src/src/ocaml-attributes.md: -------------------------------------------------------------------------------- 1 | # Attributes 2 | 3 | You can attach certain attributes to your value specifications to change their behavior. 4 | 5 | Currently, the only recognized attributes are `py_fun_name` and `py_arg_name`. 6 | 7 | You can use them to bind a Python method names and argument names to something else on the OCaml side. 8 | 9 | ## Example 10 | 11 | For example, rather than having a `Cat.__init__` method to call from the OCaml side (i.e., to make a new Python `Cat` object), you can bind the Python `__init__` function to something more idiomatic like `create` or whatever you want. 12 | 13 | ```ocaml 14 | val create : t -> ... 15 | [@@py_fun_name __init__] 16 | ``` 17 | 18 | Another common use for this is to bind the Python `__str__` method for a class to `to_string` on the OCaml side. 19 | 20 | You can do this with any function. One reason is that you may want to have some type safety with a polymorphic Python function. While you could pass in [Py.Object.t](https://mooreryan.github.io/ocaml_python_bindgen/types/#pytypes) directly, you could also use attributes to bind multiple OCaml functions to the same Python method. E.g., 21 | 22 | ```ocaml 23 | val eat : t -> num_mice:int -> unit -> unit 24 | 25 | val eat_part : t -> num_mice:float -> unit -> unit 26 | [@@py_fun_name eat] 27 | ``` 28 | 29 | In this case, we have one `eat` function for `int` and one for `float`. 30 | 31 | ## Full example 32 | 33 | For a full working example see the [attributes](https://github.com/mooreryan/ocaml_python_bindgen/tree/main/examples/attributes) example on GitHub. 34 | 35 | The linked example also shows how to use `py_arg_name`. 36 | 37 | ## Warning 38 | 39 | - If you specify the `py_fun_name` more than once, it will do something wonky. Eventually, the program will treat this as an error, but for now, it is on you to avoid doing it. 40 | - Attributes have to start a line. I.e., if you have more than one attribute, you can't put them on the same line. They must go on separate lines. 41 | -------------------------------------------------------------------------------- /_docs_src/src/of-pyobject.md: -------------------------------------------------------------------------------- 1 | # Converting `pyobjects` to OCaml Types 2 | 3 | With `pyml_bindgen`, you are generally want to set up a binding from a single Python class to a single OCaml module. 4 | 5 | E.g., 6 | 7 | ```python 8 | class Foo: 9 | def __init__(self, x) 10 | self.x = x 11 | 12 | def add1(self): 13 | self.x += 1 14 | ``` 15 | 16 | would have a corresponding module something like this 17 | 18 | ```ocaml 19 | module Foo : sig 20 | type t 21 | 22 | val of_pyobject : Pytypes.pyobject -> t 23 | val to_pyobject : t -> Pytypes.pyobject 24 | 25 | val x : t -> int 26 | 27 | val add1 : t -> unit -> unit 28 | end = struct 29 | type t = Pytypes.pyobject 30 | 31 | let of_pyobject x = x 32 | let to_pyobject x = x 33 | 34 | let add1 t () = 35 | end 36 | ``` 37 | 38 | In the above example, we don't bother checking the Python-land type of the `pyobject`. All OCaml compiler knows at compile time is that we are taking a `Pytypes.pyobject` type and getting back a `Foo.t`. 39 | 40 | Depending on how that `pyobject` was actually created elsewhere in the code, it might not actually be an instance of the `Foo` class. In this case, when you go to call the `Foo.say_hi` function in your OCaml code, you will get a runtime error. Let me give you an example. 41 | 42 | ```ocaml 43 | let i = Py.Int.of_int 1 44 | assert 45 | let foo = Foo.of_pyobject i 46 | 47 | let _ = Foo.say_hi foo () 48 | (* ERROR! *) 49 | ``` 50 | 51 | You'll get an exception: `Exception: E (, 'int' object has no attribute 'add1')`. 52 | 53 | ## Checking `pyobjects` at module boundaries 54 | 55 | 56 | While you *could* remove the `of_pyobject` function from the interface, you are often going to need it outside the module. For example, you may have a Python class `Foo` that has a method which returns an object of class `Bar`. In your OCaml code you'd need to call the `Bar.of_pyobject` method from inside the `Foo` module. 57 | 58 | Basically, you would like to have an `of_pyobject` that actually checks that the underlying Python type is what the module expects. I.e., you only want to create a `Foo.t` if the `pyobject` is a `Foo` object in Python-land. 59 | 60 | You can address this problem in the typical OCaml way (e.g., by returning `t option` or `t Or_error.t` instead of `t`) in `pyml_bindgen` as well. Let's see what I mean. 61 | 62 | `pyml_bindgen` automatically generates `of_pyobject` and `to_pyobject` functions for you (in fact, you shouldn't provide those yourself). 63 | 64 | You can generate three kinds `of_pyobject` function with `pyml_bindgen`: 65 | 66 | * No checking: `val of_pyobject : Pytypes.pyobject -> t` 67 | * `option` returning: ` val of_pyobject : Pytypes.pyobject -> t option` 68 | * [Base](https://ocaml.janestreet.com/ocaml-core/latest/doc/base/Base/Or_error/index.html) `Or_error.t` returning: ` val of_pyobject : Pytypes.pyobject -> t Or_error.t` 69 | 70 | You can choose between the three with the `--of-pyo-ret-type` option. Here is the section from the man page: 71 | 72 | ``` 73 | -r OF_PYO_RET_TYPE, --of-pyo-ret-type=OF_PYO_RET_TYPE (absent=option) 74 | Return type of the of_pyobject function. OF_PYO_RET_TYPE must be 75 | one of `no_check', `option' or `or_error'. 76 | ``` 77 | 78 | While the `option` and `Or_error.t` let you avoid a lot of potential runtime problems, they will force you to deal with potential errors each time `of_pyobject` is called, and in code generated by `pyml_bindgen` you may not realize that it is being called! 79 | 80 | Say that you generated both `Person` and `Job` modules with the `--of-pyo-ret-type=option` command line option. Then both of these modules will have `of_pyobject` functions that return `t option` rather than just `t`. 81 | 82 | *Note: For now, you can only generate one of these module signatures at a time with `pyml_bindgen`. To combine them, you'll have to run it multiple times and then combine manually.* 83 | 84 | Here is an example of code that won't work. 85 | 86 | ```ocaml 87 | module rec Person : sig 88 | type t 89 | val of_pyobject : Pytypes.pyobject -> t option 90 | (* Oops! *) 91 | val get_job : t -> unit -> Job.t 92 | ... 93 | end = struct ... end 94 | 95 | and Job : sig 96 | type t 97 | val of_pyobject : Pytypes.pyobject -> t option 98 | ... 99 | end = struct ... end 100 | ``` 101 | 102 | When `pyml_bindgen` sees a function that ends in a custom type (a module type like `Job.t`, `Person.t`, or whatever), the generated code will call that type's `of_pyobject` function to convert it to the correct OCaml type. So, for `Person.get_job` it will generate a function that calls `Job.of_pyobject` somewhere in the `get_job` implementation. Of course, `Job.of_pyobject` returns `Job.t option` and not `Job.t`. But in the `Person.get_job` signature, we've specified that `get_job` returns `Job.t` and *NOT* `Job.t option`. 103 | 104 | Now, `pyml_bindgen` will happily generate this implementation for you, but when you try to actually compile it, you will get an error about the return type of `get_job` implementation not matching the expected signature. 105 | 106 | So what do you do? Well, you have to remember that the `--of-pyo-ret-type=option` and `--of-pyo-ret-type=or_error` flags will essentially poison all generated functions that manipulate other auto-generated modules. 107 | 108 | Specifically, for this example, you can't write `val get_job : t -> unit -> Job.t`. Instead, you have to write `val get_job : t -> unit -> Job.t option`. Just so that it's clear, the reason is because `Job.of_pyobject` returns `Job.t option`, and the generated implementation of `Person.get_job` will call `Job.of_pyobject` somewhere in its body.) 109 | 110 | ## Wrap-up 111 | 112 | You have to be aware of the return types of the `of_pyobject` functions you're generating with `pyml_bindgen`. If you use `option` or `Or_error.t`, you have to remember to adjust your value specifications accordingly! 113 | -------------------------------------------------------------------------------- /_docs_src/src/recursive.md: -------------------------------------------------------------------------------- 1 | # Binding Recursive Classes 2 | 3 | You will often run into cases in which you need to bind classes that are cyclical. Here's an example: 4 | 5 | ```python 6 | class Foo: 7 | @staticmethod 8 | def make_bar(): 9 | return Bar() 10 | 11 | class Bar: 12 | @staticmethod 13 | def make_foo(): 14 | return Foo() 15 | ``` 16 | 17 | `Foo` has a method that returns a `Bar` object, and `Bar` has a method that returns a `Foo` object. 18 | 19 | While this works fine in Python, we have to be more explicit in OCaml in these kinds of situations. 20 | 21 | ## Auto-generate bindings 22 | 23 | As of version `0.4.0-SNAPSHOT`, `pyml_bindgen` ships two helper scripts for dealing with this type of thing automatically: `gen_multi` and `combine_rec_modules`. Check out the [Recursive Modules](https://github.com/mooreryan/ocaml_python_bindgen/tree/main/examples/recursive_modules) example on GitHub for how to use them. 24 | 25 | ## Semi-manually generate bindings 26 | 27 | The `pyml_bindgen` itself doesn't handle recursive modules. But it is simple enough to edit the output by hand. Let's see how. 28 | 29 | ### Value specs 30 | 31 | Since there are two classes to bind, we will make two val spec files. 32 | 33 | `foo_val_specs.txt` 34 | 35 | ```ocaml 36 | val make_bar : unit -> Bar.t 37 | ``` 38 | 39 | `bar_val_specs.txt` 40 | 41 | ```ocaml 42 | val make_foo : unit -> Foo.t 43 | ``` 44 | 45 | ### Run `pyml_bindgen` 46 | 47 | Now, run `pyml_bindgen` with some extra shell commands to make the output look nicer. 48 | 49 | ``` 50 | pyml_bindgen foo_val_specs.txt silly Foo --caml-module Foo -r no_check \ 51 | | ocamlformat --enable --name=a.ml - > lib.ml 52 | 53 | printf "\n" >> lib.ml 54 | 55 | pyml_bindgen bar_val_specs.txt silly Bar --caml-module Bar -r no_check \ 56 | | ocamlformat --enable --name=a.ml - >> lib.ml 57 | ``` 58 | 59 | ### Fix the output 60 | 61 | If you were to try and compile that code, you'd get a lot of errors including about unknown`Bar` module. 62 | 63 | To fix it, change `module Foo : sig` to `module rec Foo : sig` and `module Bar : sig` to `and Bar : sig`. 64 | 65 | Once you do that, everything will compile fine :) 66 | 67 | Here is what the output should look like: 68 | 69 | ```ocaml 70 | module rec Foo : sig 71 | 72 | ... sig ... 73 | 74 | end = struct 75 | 76 | ... impl ... 77 | 78 | end 79 | 80 | and Bar : sig 81 | 82 | ... sig ... 83 | 84 | end = struct 85 | 86 | ... impl ... 87 | 88 | end 89 | ``` 90 | 91 | ### Using the generated modules 92 | 93 | You can use the generated modules as you would any others. 94 | 95 | ```ocaml 96 | open Lib 97 | 98 | let () = Py.initialize () 99 | 100 | let (_bar : Bar.t) = Foo.make_bar () 101 | let (_foo : Foo.t) = Bar.make_foo () 102 | ``` 103 | 104 | ## Wrap-up 105 | 106 | You may come across cyclic classes when binding Python code. If you want to bind them in OCaml as it, you will need to use recursive module. This page shows you how to do it semi-manually using `pyml_bindgen`. If you would like a more automatic way to do this, see the [Recursive Modules](https://github.com/mooreryan/ocaml_python_bindgen/tree/main/examples/recursive_modules) example on GitHub. 107 | -------------------------------------------------------------------------------- /_docs_src/src/robots.txt: -------------------------------------------------------------------------------- 1 | Sitemap: https://mooreryan.github.io/ocaml_python_bindgen/sitemap.xml 2 | -------------------------------------------------------------------------------- /_docs_src/src/rules-overview.md: -------------------------------------------------------------------------------- 1 | # Rules Overview 2 | 3 | The following pages are an informal description of some of the rules you have to follow when using `pyml_bindgen`. 4 | 5 | I will try and keep them updated and (reasonably) complete. However, if you really want to know how something is working, you may want to have a look at the [tests](https://github.com/mooreryan/ocaml_python_bindgen/tree/main/test) or the [examples](https://github.com/mooreryan/ocaml_python_bindgen/tree/main/examples). 6 | -------------------------------------------------------------------------------- /_docs_src/src/split-ml-mli.md: -------------------------------------------------------------------------------- 1 | # Splitting generated modules 2 | 3 | You can split the generated module into separate `ml` and `mli` files using the `--split-caml-module` option. 4 | 5 | For a full example of this, see the [example](https://github.com/mooreryan/ocaml_python_bindgen/tree/main/examples/gen_ml_and_mli) on GitHub. 6 | -------------------------------------------------------------------------------- /_docs_src/src/static-methods.md: -------------------------------------------------------------------------------- 1 | # Class & Static Methods; Functions 2 | 3 | Value specs for class/static methods look like this: 4 | 5 | ```ocaml 6 | val f : a:'a -> ?b:'b -> ... -> unit -> 'c 7 | ``` 8 | 9 | ## Rules 10 | 11 | * The final function argument (penultimate type expression) must be `unit`. 12 | * The return type can be any of the [types](./types.md) mentioned earlier. 13 | * The remaining function arguments must either be named or optional. The types of these arguments can be any of the types mentioned earlier. 14 | 15 | ## Examples 16 | 17 | ```ocaml 18 | val add_item : fruit:string -> price:float -> unit -> unit 19 | val subtract : x:int -> ?y:int -> unit -> int 20 | ``` 21 | 22 | ## Binding `__init__` 23 | 24 | `__init__` methods are called when constructing new Python objects. Here is an example. 25 | 26 | Python: 27 | 28 | ```python 29 | class Person: 30 | def __init__(self, name, age): 31 | self.name = name 32 | self.age = age 33 | ``` 34 | 35 | And the OCaml binding.... 36 | 37 | ```ocaml 38 | val __init__ : name:string -> age:int -> unit -> t 39 | ``` 40 | 41 | If you want to generate functions that ensure the class is correct, you can return `t option` or `t Or_error.t` instead. 42 | 43 | ### Using a different name 44 | 45 | You can use a more natural name for the `__init__` function. E.g., something like `create` by using [attributs](./ocaml-attributes.md). 46 | 47 | ```ocaml 48 | val create : name:string -> age:int -> unit -> t 49 | [@@py_fun_name __init__] 50 | ``` 51 | 52 | ## Functions 53 | 54 | You can also bind functions that are not associated with a class. 55 | 56 | The rules are the same for the class and static methods. To tell `pyml_bindgen` that you are actually binding module functions rather than class methods, you have to pass in a command line option `--associated-with module`. 57 | -------------------------------------------------------------------------------- /_docs_src/src/todo.md: -------------------------------------------------------------------------------- 1 | # To do 2 | 3 | Whoops, you hit a page that isn't finished yet! 4 | 5 | This documentation is a work in progress. If you feel as though 6 | something is missing, feel free to [open an 7 | issue](https://github.com/mooreryan/ocaml_python_bindgen/issues) 8 | on the [pyml_bindgen](https://github.com/mooreryan/ocaml_python_bindgen) 9 | repository. 10 | -------------------------------------------------------------------------------- /_docs_src/src/tuples.md: -------------------------------------------------------------------------------- 1 | # Handling Tuples 2 | 3 | As of version 0.3.0, you can handle certain types of tuples directly. 4 | 5 | - You can now bind tuples with 2, 3, 4, or 5 elements. 6 | - They can be passed in as arguments, or returned from functions. 7 | - Only basic types and Python objects are allowed in tuples. 8 | - You can also put tuples inside of collections, e.g., `(int * string) list`, but not Options or Or_errors. 9 | 10 | If you need something more complicated then that, you will have to use some of the same tricks I talk about in the [dictionaries](dictionaries.md) or [dictionaries-2](dictionaries-2.md) help pages. 11 | 12 | ## Examples 13 | 14 | You can find examples of binding tuples [here](https://github.com/mooreryan/ocaml_python_bindgen/tree/main/examples/tuples). 15 | -------------------------------------------------------------------------------- /_docs_src/src/types.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | Not all OCaml types are allowed. 4 | 5 | There are a lot of [tests](https://github.com/mooreryan/pyml_bindgen/tree/main/test) that exercise the rules here. 6 | 7 | ## Function arguments 8 | 9 | For function arguments, you can use 10 | 11 | - `float` 12 | - `string` 13 | - `bool` 14 | - `t` (i.e., the main type of the current module) 15 | - Other module types (e.g., `Span.t`, `Doc.t`, `Yummy.Apple_pie.t`) 16 | - Arrays of any of the above types 17 | - Lists of any of the above types 18 | - Seq.t of any of the above types 19 | - `'a option`, `'a option array`, `'a option list`, `'a option Seq.t` 20 | - `Pytypes.pyobject` or `Py.Object.t` if you need to deal with `pytypes` directly 21 | - Certain kinds of [tuples](tuples.md) 22 | 23 | Note that your custom types must be newly minted modules. E.g., 24 | 25 | ```ocaml 26 | (* This is okay *) 27 | module Doc = struct 28 | type t 29 | let of_pyobject ... 30 | let to_pyobject ... 31 | ... 32 | end 33 | 34 | (* But this is not *) 35 | type doc 36 | let doc_of_pyobject ... 37 | let doc_to_pyobject ... 38 | ``` 39 | 40 | ## Return types 41 | 42 | For return types, you can use all of the above types plus `unit`, and `'a Or_error.t` for types `'a` other than `unit`. However, you cannot use `unit array`, `unit list`, or `unit Seq.t`. 43 | 44 | You can also return many kinds of tuples directly. See [here](tuples.md). 45 | 46 | ## Nesting 47 | 48 | Note: currently, you're not allowed to have **nested** `array`, `list`, `Seq.t`, or `Or_error.t`. If you need them, you will have to bind those functions by hand :) 49 | 50 | E.g., `'a array list` will fail. 51 | 52 | You are allowed to nest `'a option` in arrays, lists, and `Seq.t`s (e.g., `'a option list`); however, this will not work with `Or_error.t`. 53 | 54 | ## Pytypes 55 | 56 | Sometimes you may want to deal directly with `Pytypes.pyobject` (a.k.a. `Py.Object.t`). 57 | 58 | Maybe you have a Python function that is truly polymorphic, or you just don't feel like giving a function a specific OCaml type for whatever reason. Regardless, you can use `Pytypes.pyobject` or `Py.Object.t` for this. 59 | 60 | Of course, you will be leaking a bit of the `pyml` implementation into your API, but sometimes that is unavoidable, or just more convenient than dealing with it in another way. 61 | 62 | Note that you currently are not allowed to nest `pytypes` in any of the containers or monads. 63 | 64 | ## Tuples 65 | 66 | You can handle many kinds of tuples directly. See [here](tuples.md). 67 | 68 | ## Dictionaries 69 | 70 | See [here](dictionaries.md) and [here](dictionaries-2.md) for examples of binding dictionaries. 71 | 72 | Alternatively, you could mark them as `Pytypes.pyobject` or `Py.Object.t` and let the caller deal with them in some way. 73 | 74 | ## Placeholders 75 | 76 | There are two placeholders you can use: `todo` and `not_implemented`. 77 | 78 | If you're binding a large library and you aren't planning on implementing a function, but you want it in the signature for whatever reason, you can use `not_implemented`. If you are planning to come back and implement a function later, you can use `todo`. 79 | 80 | ```ocaml 81 | val f : 'a todo 82 | val g : 'a not_implemented 83 | ``` 84 | 85 | These are special in that you can't just use them anywhere, it has to be exactly as above. 86 | 87 | The generated functions for the above signatures will be like this: 88 | 89 | ```ocaml 90 | let f () = failwith "todo: f" 91 | let g () = failwith "not implemented: g" 92 | ``` 93 | 94 | So if a user actually calls these functions, the program will fail at runtime. 95 | -------------------------------------------------------------------------------- /bench/adder.py: -------------------------------------------------------------------------------- 1 | class Adder: 2 | @staticmethod 3 | def add(x, y): 4 | return x + y 5 | -------------------------------------------------------------------------------- /bench/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (names bench_import_module) 5 | (libraries core core_bench core_unix.command_unix pyml)) 6 | 7 | ; TODO: clean up the unused stuff rather than silence warning here 8 | 9 | (env 10 | (dev 11 | (flags 12 | (:standard -w -32)))) 13 | 14 | ;;; Uncomment this if you need to regenerate the lib. 15 | ; (rule 16 | ; (target lib.ml) 17 | ; (mode 18 | ; (promote (until-clean))) 19 | ; (action 20 | ; (progn 21 | ; (with-stdout-to 22 | ; lib.ml 23 | ; (progn 24 | ; (run %{bin:pyml_bindgen} val_specs.txt adder Adder --caml-module Adder) 25 | ; (run %{bin:pyml_bindgen} val_specs.txt adder Adder --caml-module Adder_embedded --embed-python-source=adder.py))) 26 | ; (run ocamlformat --inplace --name lib.ml lib.ml)))) 27 | -------------------------------------------------------------------------------- /bench/val_specs.txt: -------------------------------------------------------------------------------- 1 | val add : x:int -> y:int -> unit -> int 2 | -------------------------------------------------------------------------------- /bin/combine_rec_modules.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | 3 | module Cli = struct 4 | open Cmdliner 5 | 6 | (* Use string rather than file so we can do the nicer file checking. *) 7 | let files_term = 8 | let doc = 9 | "OCaml source files. You can also pass in /dev/stdin to read from \ 10 | standard input." 11 | in 12 | Arg.(non_empty & pos_all string [] & info [] ~docv:"FILE" ~doc) 13 | 14 | let info = 15 | let doc = "combine recursive modules into a single file" in 16 | let man = 17 | [ 18 | `S Manpage.s_description; 19 | `P 20 | "You often need to generate recursive modules when binding cyclic \ 21 | Python classes. Since pyml_bindgen doesn't allow you to generate \ 22 | recursive modules automatically, you can use this tool to convert \ 23 | them. It combines multiple pyml_bindgen generated OCaml modules \ 24 | into a single file."; 25 | `P 26 | "While you could combine generated modules by hand, this tool helps \ 27 | speed up the process when combining a lot of modules, or when you \ 28 | need to automate the process (e.g., in a Dune rule or shell \ 29 | script)."; 30 | `S Manpage.s_examples; 31 | `P 32 | "Imagine you generated the files a.ml and b.ml with pyml_bindgen. \ 33 | Module A refers to module B and module B refers to module A, so \ 34 | they are recursive modules. However, pyml_bindgen doesn't \ 35 | automatically generate them as recursive modules. Instead, you can \ 36 | use combine_rec_modules."; 37 | `P "a.ml contents:"; 38 | `Pre " module A : sig ... end = struct ... end"; 39 | `P "b.ml contents:"; 40 | `Pre " module B : sig ... end = struct ... end"; 41 | `P "Run combine_rec_modules:"; 42 | `Pre " \\$ combine_rec_modules a.ml b.ml > lib.ml"; 43 | `P "Then lib.ml contents will be:"; 44 | `Pre 45 | "module rec A : sig ... end = struct ... end\n\ 46 | and B : sig ... end = struct ... end"; 47 | `P "==="; 48 | `P 49 | "You will often use this program with combine_rec_modules and \ 50 | ocamlformat."; 51 | `Pre 52 | " \\$ gen_multi cli_specs.tsv \\\\ \n\ 53 | \ | combine_rec_modules /dev/stdin \\\\ \n\ 54 | \ | ocamlformat --name a.ml - \\\\ \n\ 55 | \ > lib.ml"; 56 | `S Manpage.s_bugs; 57 | `P 58 | "Please report any bugs or issues on GitHub. \ 59 | (https://github.com/mooreryan/pyml_bindgen/issues)"; 60 | `S Manpage.s_see_also; 61 | `P 62 | "For full documentation, please see the GitHub page. \ 63 | (https://github.com/mooreryan/pyml_bindgen)"; 64 | `S Manpage.s_authors; 65 | `P "Ryan M. Moore "; 66 | ] 67 | in 68 | Cmd.info "combine_rec_modules" ~version:Lib.Version.version ~doc ~man 69 | ~exits:[] 70 | 71 | let parse_argv () = 72 | match Cmd.eval_value @@ Cmd.v info files_term with 73 | | Ok (`Ok files) -> Ok files 74 | | Ok `Help | Ok `Version -> Error 0 75 | | Error _ -> Error 1 76 | end 77 | 78 | let module_re = Re.Perl.compile_pat "module ([a-zA-Z0-9_]+)" 79 | 80 | let first_module name = "module rec " ^ name 81 | 82 | let non_first_module name = "and " ^ name 83 | 84 | let fix_module_line modules_seen group = 85 | let module_name = Re.Group.get group 1 in 86 | let is_first_module = modules_seen = 0 in 87 | if is_first_module then first_module module_name 88 | else non_first_module module_name 89 | 90 | let process_module_line modules_seen line = 91 | let new_line = Re.replace module_re line ~f:(fix_module_line modules_seen) in 92 | Stdio.print_endline new_line; 93 | modules_seen + 1 94 | 95 | let process_non_module_line modules_seen line = 96 | Stdio.print_endline line; 97 | modules_seen 98 | 99 | let process_line modules_seen line = 100 | if Re.execp module_re line then process_module_line modules_seen line 101 | else process_non_module_line modules_seen line 102 | 103 | let process_file modules_seen fname = 104 | Stdio.In_channel.with_file fname ~f:(fun ic -> 105 | Stdio.In_channel.fold_lines ic ~init:modules_seen ~f:process_line) 106 | 107 | let check_fnames fnames = 108 | let errors = 109 | List.filter_map fnames ~f:(fun fname -> 110 | if Caml.Sys.file_exists fname then None else Some fname) 111 | in 112 | match errors with 113 | | [] -> () 114 | | [ fname ] -> 115 | let msg = [%string "ERROR -- File %{fname} does not exist"] in 116 | Bin_utils.exit ~code:1 msg 117 | | fnames -> 118 | let fnames = String.concat fnames ~sep:", " in 119 | let msg = [%string "ERROR -- These files do not exist: %{fnames}"] in 120 | Bin_utils.exit ~code:1 msg 121 | 122 | (* TODO this will exit with an error code, but stuff will still be printed to 123 | the outfile. Could be confusing. *) 124 | let check_modules_seen = function 125 | | 0 -> 126 | Bin_utils.exit ~code:1 127 | [%string "ERROR -- I didn't see any modules in the input files"] 128 | | 1 -> 129 | Bin_utils.exit ~code:1 130 | [%string "ERROR -- I only saw one module in the input files"] 131 | | _n -> () 132 | 133 | let run fnames = 134 | check_fnames fnames; 135 | let modules_seen = List.fold fnames ~init:0 ~f:process_file in 136 | check_modules_seen modules_seen 137 | 138 | let main () = 139 | match Cli.parse_argv () with 140 | | Ok fnames -> run fnames 141 | | Error exit_code -> Caml.exit exit_code 142 | 143 | let () = main () 144 | -------------------------------------------------------------------------------- /bin/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (package pyml_bindgen) 3 | (public_names pyml_bindgen combine_rec_modules gen_multi) 4 | (names pyml_bindgen combine_rec_modules gen_multi) 5 | (libraries base cmdliner lib re stdio) 6 | (instrumentation 7 | (backend bisect_ppx)) 8 | (preprocess 9 | (pps ppx_let ppx_string))) 10 | -------------------------------------------------------------------------------- /bin/gen_multi.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | 3 | module Cli = struct 4 | open Cmdliner 5 | 6 | let file_term = 7 | let doc = 8 | "TSV file with CLI opts. The first line (header) is ignored. The \ 9 | columns should be should be: signatures [tab] py_module [tab] py_class \ 10 | [tab] associated_with [tab] caml_module [tab] embed_python_source [tab] \ 11 | of_pyo_ret_type." 12 | in 13 | Arg.( 14 | required & pos 0 (some non_dir_file) None & info [] ~docv:"CLI_OPTS" ~doc) 15 | 16 | let info = 17 | let doc = "generate multiple pyml_bindgen bindings with one command" in 18 | let man = 19 | [ 20 | `S Manpage.s_description; 21 | `P 22 | "In some cases, it can be simpler to use a single command to \ 23 | generate multiple bindings."; 24 | `P 25 | "The input file is a 7 column TSV file. Each column matches one of \ 26 | the arguments to pyml_bindgen that you would give on the command \ 27 | line. The order of the columns must be: signatures, py_module, \ 28 | py_class, associated_with, caml_module, embed_python_source, and \ 29 | of_pyo_return_type."; 30 | `P 31 | "You fill in the columns more or less in the same way as you would \ 32 | pass things in to pyml_bindgen. For optional flags, if you don't \ 33 | want to apply then, you can leave them blank, or you can use NA or \ 34 | na."; 35 | `S Manpage.s_examples; 36 | `P 37 | "You will often use this program with combine_rec_modules and \ 38 | ocamlformat."; 39 | `Pre 40 | " \\$ gen_multi cli_specs.tsv \\\\ \n\ 41 | \ | combine_rec_modules /dev/stdin \\\\ \n\ 42 | \ | ocamlformat --name a.ml - \\\\ \n\ 43 | \ > lib.ml"; 44 | `S Manpage.s_bugs; 45 | `P 46 | "Please report any bugs or issues on GitHub. \ 47 | (https://github.com/mooreryan/pyml_bindgen/issues)"; 48 | `S Manpage.s_see_also; 49 | `P 50 | "For full documentation, please see the GitHub page. \ 51 | (https://github.com/mooreryan/pyml_bindgen)"; 52 | `S Manpage.s_authors; 53 | `P "Ryan M. Moore "; 54 | ] 55 | in 56 | Cmd.info "gen_multi" ~version:Lib.Version.version ~doc ~man ~exits:[] 57 | 58 | let parse_argv () = 59 | match Cmd.eval_value @@ Cmd.v info file_term with 60 | | Ok (`Ok files) -> Ok files 61 | | Ok `Help | Ok `Version -> Error 0 62 | | Error _ -> Error 1 63 | end 64 | 65 | let ok_or_abort x = 66 | Or_error.iter_error x ~f:(fun err -> 67 | Bin_utils.exit ~code:1 @@ Error.to_string_hum err) 68 | 69 | (* TODO just fix the run function so it won't raise... *) 70 | let run_wrapper opts = 71 | Or_error.join @@ Or_error.try_with (fun () -> Bin_utils.run opts) 72 | 73 | (* Each row of the input file is a pyml_bindgen CLI specification. *) 74 | let get_cli_opts fname = 75 | let open Stdio in 76 | let open Or_error.Let_syntax in 77 | let opts = 78 | In_channel.with_file fname ~f:(fun ic -> 79 | Or_error.all @@ List.rev @@ snd 80 | @@ In_channel.fold_lines ic ~init:(0, []) ~f:(fun (i, things) line -> 81 | if i > 0 then (i + 1, Main_cli.opts_of_string line :: things) 82 | else (i + 1, things))) 83 | in 84 | match%bind opts with 85 | | [] -> Or_error.error_string "Got no options" 86 | | opts -> Or_error.return opts 87 | 88 | let run fname = 89 | match get_cli_opts fname with 90 | | Ok opts -> ok_or_abort @@ Or_error.all_unit @@ List.map opts ~f:run_wrapper 91 | | Error err -> Bin_utils.exit ~code:1 (Error.to_string_hum err) 92 | 93 | let main () = 94 | match Cli.parse_argv () with 95 | | Ok fname -> run fname 96 | | Error exit_code -> Caml.exit exit_code 97 | 98 | let () = main () 99 | -------------------------------------------------------------------------------- /bin/main_cli.mli: -------------------------------------------------------------------------------- 1 | open! Base 2 | 3 | type opts = { 4 | signatures : string; 5 | py_module : string; 6 | py_class : string; 7 | associated_with : [ `Class | `Module ]; 8 | caml_module : string option; 9 | split_caml_module : string option; 10 | embed_python_source : string option; 11 | of_pyo_ret_type : [ `No_check | `Option | `Or_error ]; 12 | } 13 | 14 | val opts_of_string : string -> opts Or_error.t 15 | 16 | val parse_argv : unit -> (opts, int) Result.t 17 | (** If successful, return the [opts]. If failure, return exit code. *) 18 | -------------------------------------------------------------------------------- /bin/pyml_bindgen.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | 3 | (* TODO better to move this check into the Cmdliner section. *) 4 | let check_opts (opts : Main_cli.opts) = 5 | match (opts.caml_module, opts.split_caml_module) with 6 | | None, Some _ -> 7 | Bin_utils.exit ~code:1 8 | "ERROR: --split-caml-module was given but --caml-module was not" 9 | | None, None | Some _, None | Some _, Some _ -> () 10 | 11 | let main () = 12 | match Main_cli.parse_argv () with 13 | | Ok opts -> ( 14 | check_opts opts; 15 | match Bin_utils.run opts with 16 | | Ok _ -> Caml.exit 0 17 | (* TODO non-zero exit code here? *) 18 | | Error err -> Stdio.prerr_endline @@ Error.to_string_hum err) 19 | | Error exit_code -> Caml.exit exit_code 20 | 21 | let () = main () 22 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | 3 | (generate_opam_files true) 4 | 5 | (cram enable) 6 | 7 | (name pyml_bindgen) 8 | 9 | (version 0.4.1) 10 | 11 | (maintainers "Ryan M. Moore") 12 | 13 | (authors "Ryan M. Moore") 14 | 15 | (homepage "https://github.com/mooreryan/ocaml_python_bindgen") 16 | 17 | (source 18 | (github mooreryan/ocaml_python_bindgen)) 19 | 20 | (documentation "https://mooreryan.github.io/ocaml_python_bindgen/") 21 | 22 | (bug_reports "https://github.com/mooreryan/ocaml_python_bindgen/issues") 23 | 24 | (package 25 | (name pyml_bindgen-dev) 26 | (synopsis "Development package for pyml_bindgen") 27 | (allow_empty) 28 | (depends 29 | (ocaml 30 | (>= "4.14")) 31 | bisect_ppx 32 | (core 33 | (>= "v0.15")) 34 | (core_bench 35 | (>= "v0.15")) 36 | (core_unix 37 | (>= "v0.15")) 38 | (ocamlformat 39 | (and 40 | (>= "0.23") 41 | (< "0.24"))) 42 | (ocaml-lsp-server 43 | (>= "1.13")) 44 | pyml)) 45 | 46 | (package 47 | (name pyml_bindgen) 48 | (synopsis "Generate pyml bindings from OCaml value specifications") 49 | (depends 50 | ; Runtime deps 51 | (angstrom 52 | (>= "0.15.0")) 53 | (base 54 | (>= "v0.12")) 55 | (cmdliner 56 | (>= "1.1.0")) 57 | (ppx_let 58 | (>= "v0.12")) 59 | (ppx_sexp_conv 60 | (>= "v0.12")) 61 | (ppx_string 62 | (>= "v0.12")) 63 | (re 64 | (>= "1.10.0")) 65 | (stdio 66 | (>= "v0.12")) 67 | (ocaml 68 | (>= "4.08.0")) 69 | ; Test-only deps 70 | (conf-python-3-dev 71 | (and 72 | (>= "1") 73 | :with-test)) 74 | (base_quickcheck 75 | (and 76 | (>= "v0.12") 77 | :with-test)) 78 | (ocamlformat 79 | (and 80 | (>= "0.23") 81 | (< "0.24") 82 | :with-test)) 83 | (ppx_assert 84 | (and 85 | (>= "v0.12") 86 | :with-test)) 87 | (ppx_inline_test 88 | (and 89 | (>= "v0.12") 90 | :with-test)) 91 | (ppx_expect 92 | (and 93 | (>= "v0.12") 94 | :with-test)) 95 | (pyml :with-test) 96 | (shexp 97 | (and 98 | (>= "v0.14") 99 | :with-test)))) 100 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | In this directory you will find some full examples. Each example contains 4 | 5 | * Value specification files to specify the bindings 6 | * Python code/modules that we want to bind 7 | * Tests that are run whenever you run `dune test` in the source directory 8 | * Automatic generation of OCaml bindings using Dune [rules](https://dune.readthedocs.io/en/stable/dune-files.html#rule) 9 | 10 | The auto-generation of bindings is particularly sweet! If you update any of the spec files, dune will pick up the change and regenerate the OCaml binding code automatically when you run `dune build`! 11 | -------------------------------------------------------------------------------- /examples/attributes/README.md: -------------------------------------------------------------------------------- 1 | # Attributes 2 | 3 | This directory has an example of using the `py_fun_name` and `py_arg_name` attributes. 4 | 5 | ## `py_fun_name` 6 | 7 | `py_fun_name` allows you to use different names for functions on the OCaml side than the original Python library had used. 8 | 9 | For example, rather than having a `Cat.__init__` method to call from the OCaml side (i.e., to make a new Python `Cat` object), you can bind the Python `__init__` function to something more idiomatic like `create` or whatever you want. 10 | 11 | ```ocaml 12 | val create : t -> ... 13 | [@@py_fun_name __init__] 14 | ``` 15 | 16 | Another common use for this is to bind the Python `__str__` method for a class to `to_string` on the OCaml side. 17 | 18 | You can do this with any function. One reason is that you may want to have some type safety with a polymorphic Python function. While you could pass in [Py.Object.t](https://mooreryan.github.io/ocaml_python_bindgen/types/#pytypes) directly, you could also use attributes to bind multiple OCaml functions to the same Python method. E.g., 19 | 20 | ```ocaml 21 | val eat : t -> num_mice:int -> unit -> unit 22 | 23 | val eat_part : t -> num_mice:float -> unit -> unit 24 | [@@py_fun_name eat] 25 | ``` 26 | 27 | In this case, we have one `eat` function for `int` and one for `float`. 28 | 29 | ## `py_arg_name` 30 | 31 | `py_arg_name` allows you to use different argument names on the OCaml side from those that are used on the Python side. 32 | 33 | For example, you may have a Python function that has an argument name that is the same as some reserved OCaml keyword. In this case, you can use `py_arg_name` to map it to something else on the OCaml side. 34 | 35 | ```ocaml 36 | val f : t -> method_:string -> unit -> string 37 | [@@py_arg_name method_ method] 38 | ``` 39 | 40 | As you see, the attribute is followed by two items, the first is the argument name on the OCaml side, and the second is the argument name on the Python side (i.e., as it will be called in Python). 41 | 42 | ## Multiple attributes 43 | 44 | You can use multiple attributes on a single val spec. Here is one from this example project. 45 | 46 | ```ocaml 47 | val say_this : t -> w:string -> x:string -> y:string -> z:string -> unit -> string 48 | [@@py_fun_name say] 49 | [@@py_arg_name w a] 50 | [@@py_arg_name x b] 51 | [@@py_arg_name y c] 52 | [@@py_arg_name z d] 53 | ``` 54 | 55 | Here is the python function. 56 | 57 | ```python 58 | class Cat: 59 | 60 | ... 61 | 62 | def say(self, a, b, c, d): 63 | return(f'{self.name} says {a}, {b}, {c} and {d}.') 64 | ``` 65 | 66 | As you see, the Python function is called `say` and not `say_this`. Also, the arguments to the Python function are `a`, `b`, `c`, and `d`, whereas we bind them to `w`, `x`, `y`, and `z` on the OCaml side. 67 | 68 | _Note: Multiple attributes on the same value spec must go on separate lines._ 69 | -------------------------------------------------------------------------------- /examples/attributes/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (name run) 5 | (libraries examples_attributes_lib pyml)) 6 | -------------------------------------------------------------------------------- /examples/attributes/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | -------------------------------------------------------------------------------- /examples/attributes/lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (name examples_attributes_lib) 5 | (libraries pyml)) 6 | 7 | (rule 8 | ;; The file produced by this rule will be called `examples_attributes_lib.ml`. 9 | (target examples_attributes_lib.ml) 10 | ;; Put the resulting examples_attributes_lib.ml file in this directory. When you run 11 | ;; `dune clean`, it will be removed. 12 | (mode 13 | (promote (until-clean))) 14 | ;; We need to give the action access to the files in the `specs` dir. 15 | (deps 16 | (source_tree specs) 17 | ;; We also need to give access to the Python source directory. 18 | (source_tree ../py)) 19 | (action 20 | ;; `pyml_bindgen` outputs directly to stdout, so we need to redirect 21 | ;; the stdout to `examples_attributes_lib.ml`. We do this using `with-stdout-to`. 22 | (with-stdout-to 23 | ;; This is the name of the file to which the output of the 24 | ;; following command will be directed. 25 | examples_attributes_lib.ml 26 | ;; This step isn't necessary, but I use `ocamlformat` for 27 | ;; everything, so I would like to have the output of 28 | ;; `pyml_bindgen` also processed by `ocamlformat`. `pipe-stdout` 29 | ;; takes the stdout of the first `run` command and pipes it to the 30 | ;; stdin of the second `run` command. 31 | (pipe-stdout 32 | ;; Here is the acutal `pyml_bindgen` command to generate the 33 | ;; bindings. 34 | (run 35 | %{bin:pyml_bindgen} 36 | specs/cat.txt 37 | cat 38 | Cat 39 | --caml-module 40 | Cat 41 | --of-pyo-ret-type 42 | no_check 43 | ;; This option tells `pyml_bindgen` that you want to embed 44 | ;; the Python code into the generated OCaml module. It will 45 | ;; be evaluated at runtime. Note how we refer to the path as 46 | ;; specified in `source_tree` above. 47 | --embed-python-source 48 | ../py/cat.py) 49 | ;; And finally, process the bindings code with ocamlformat. 50 | (run ocamlformat --name examples_attributes_lib.ml -))))) 51 | -------------------------------------------------------------------------------- /examples/attributes/lib/examples_attributes_lib.ml: -------------------------------------------------------------------------------- 1 | module Cat : sig 2 | type t 3 | 4 | val of_pyobject : Pytypes.pyobject -> t 5 | val to_pyobject : t -> Pytypes.pyobject 6 | val create : name:string -> unit -> t 7 | val to_string : t -> unit -> string 8 | val eat : t -> num_mice:int -> unit -> unit 9 | val eat_part : t -> num_mice:float -> unit -> unit 10 | val jump : t -> how_high:int -> unit -> unit 11 | val climb : t -> how_high:int -> unit -> unit 12 | 13 | val say_this : 14 | t -> w:string -> x:string -> y:string -> z:string -> unit -> string 15 | end = struct 16 | let filter_opt l = List.filter_map Fun.id l 17 | 18 | let py_module = 19 | lazy 20 | (let source = 21 | {pyml_bindgen_string_literal|class Cat: 22 | def __init__(self, name): 23 | self.name = name 24 | self.hunger = 0 25 | 26 | def __str__(self): 27 | return(f'Cat -- name: {self.name}, hunger: {self.hunger}') 28 | 29 | def eat(self, num_mice=1): 30 | self.hunger -= (num_mice * 5) 31 | if self.hunger < 0: 32 | self.hunger = 0 33 | 34 | def jump(self, how_high=1): 35 | if how_high > 0: 36 | self.hunger += how_high 37 | 38 | def say(self, a, b, c, d): 39 | return(f'{self.name} says {a}, {b}, {c} and {d}.') 40 | |pyml_bindgen_string_literal} 41 | in 42 | let filename = 43 | {pyml_bindgen_string_literal|../py/cat.py|pyml_bindgen_string_literal} 44 | in 45 | let bytecode = Py.compile ~filename ~source `Exec in 46 | Py.Import.exec_code_module 47 | {pyml_bindgen_string_literal|cat|pyml_bindgen_string_literal} bytecode) 48 | 49 | let import_module () = Lazy.force py_module 50 | 51 | type t = Pytypes.pyobject 52 | 53 | let of_pyobject pyo = pyo 54 | let to_pyobject x = x 55 | 56 | let create ~name () = 57 | let callable = Py.Module.get (import_module ()) "Cat" in 58 | let kwargs = filter_opt [ Some ("name", Py.String.of_string name) ] in 59 | of_pyobject @@ Py.Callable.to_function_with_keywords callable [||] kwargs 60 | 61 | let to_string t () = 62 | let callable = Py.Object.find_attr_string t "__str__" in 63 | let kwargs = filter_opt [] in 64 | Py.String.to_string 65 | @@ Py.Callable.to_function_with_keywords callable [||] kwargs 66 | 67 | let eat t ~num_mice () = 68 | let callable = Py.Object.find_attr_string t "eat" in 69 | let kwargs = filter_opt [ Some ("num_mice", Py.Int.of_int num_mice) ] in 70 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 71 | 72 | let eat_part t ~num_mice () = 73 | let callable = Py.Object.find_attr_string t "eat" in 74 | let kwargs = filter_opt [ Some ("num_mice", Py.Float.of_float num_mice) ] in 75 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 76 | 77 | let jump t ~how_high () = 78 | let callable = Py.Object.find_attr_string t "jump" in 79 | let kwargs = filter_opt [ Some ("how_high", Py.Int.of_int how_high) ] in 80 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 81 | 82 | let climb t ~how_high () = 83 | let callable = Py.Object.find_attr_string t "jump" in 84 | let kwargs = filter_opt [ Some ("how_high", Py.Int.of_int how_high) ] in 85 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 86 | 87 | let say_this t ~w ~x ~y ~z () = 88 | let callable = Py.Object.find_attr_string t "say" in 89 | let kwargs = 90 | filter_opt 91 | [ 92 | Some ("a", Py.String.of_string w); 93 | Some ("b", Py.String.of_string x); 94 | Some ("c", Py.String.of_string y); 95 | Some ("d", Py.String.of_string z); 96 | ] 97 | in 98 | Py.String.to_string 99 | @@ Py.Callable.to_function_with_keywords callable [||] kwargs 100 | end 101 | -------------------------------------------------------------------------------- /examples/attributes/lib/specs/cat.txt: -------------------------------------------------------------------------------- 1 | val create : name:string -> unit -> t 2 | [@@py_fun_name __init__] 3 | 4 | val to_string : t -> unit -> string 5 | [@@py_fun_name __str__] 6 | 7 | val eat : t -> num_mice:int -> unit -> unit 8 | 9 | val eat_part : t -> num_mice:float -> unit -> unit 10 | [@@py_fun_name eat] 11 | 12 | val jump : t -> how_high:int -> unit -> unit 13 | 14 | val climb : t -> how_high:int -> unit -> unit 15 | [@@py_fun_name jump] 16 | 17 | val say_this : t -> w:string -> x:string -> y:string -> z:string -> unit -> string 18 | [@@py_fun_name say] 19 | [@@py_arg_name w a] 20 | [@@py_arg_name x b] 21 | [@@py_arg_name y c] 22 | [@@py_arg_name z d] -------------------------------------------------------------------------------- /examples/attributes/py/cat.py: -------------------------------------------------------------------------------- 1 | class Cat: 2 | def __init__(self, name): 3 | self.name = name 4 | self.hunger = 0 5 | 6 | def __str__(self): 7 | return(f'Cat -- name: {self.name}, hunger: {self.hunger}') 8 | 9 | def eat(self, num_mice=1): 10 | self.hunger -= (num_mice * 5) 11 | if self.hunger < 0: 12 | self.hunger = 0 13 | 14 | def jump(self, how_high=1): 15 | if how_high > 0: 16 | self.hunger += how_high 17 | 18 | def say(self, a, b, c, d): 19 | return(f'{self.name} says {a}, {b}, {c} and {d}.') 20 | -------------------------------------------------------------------------------- /examples/attributes/run.ml: -------------------------------------------------------------------------------- 1 | open Examples_attributes_lib 2 | 3 | let () = Py.initialize () 4 | 5 | let cat = Cat.create ~name:"Sam" () 6 | 7 | let () = Cat.climb cat ~how_high:20 () 8 | 9 | let () = Cat.eat_part cat ~num_mice:0.2 () 10 | 11 | let () = Cat.eat cat ~num_mice:2 () 12 | 13 | let () = print_endline @@ Cat.to_string cat () 14 | 15 | let () = 16 | print_endline 17 | @@ Cat.say_this cat ~w:"first" ~x:"second" ~y:"third" ~z:"fourth" () 18 | 19 | let () = print_endline "done" 20 | -------------------------------------------------------------------------------- /examples/attributes/test/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (deps ../run.exe)) 5 | -------------------------------------------------------------------------------- /examples/attributes/test/run.t: -------------------------------------------------------------------------------- 1 | Run the executable. 2 | 3 | $ ../run.exe 4 | Cat -- name: Sam, hunger: 9.0 5 | Sam says first, second, third and fourth. 6 | done 7 | -------------------------------------------------------------------------------- /examples/embedding_python_source/README.md: -------------------------------------------------------------------------------- 1 | # Embedding Python Source Code 2 | 3 | This example is pretty similar to the [quick start](https://github.com/mooreryan/ocaml_python_bindgen/tree/main/examples/quick_start) example. 4 | 5 | This one shows you how to embed the Python code directly into the generated OCaml module. It will then be evaluated at run time. 6 | 7 | To do this, you can use the `--embed-python-source` option. See this library's `dune` file for how to set it up. 8 | -------------------------------------------------------------------------------- /examples/embedding_python_source/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (name run) 5 | (libraries embedding_py_source_lib pyml)) 6 | -------------------------------------------------------------------------------- /examples/embedding_python_source/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | -------------------------------------------------------------------------------- /examples/embedding_python_source/lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (name embedding_py_source_lib) 5 | (libraries pyml)) 6 | 7 | (rule 8 | ;; The file produced by this rule will be called `math.ml`. 9 | (target math.ml) 10 | ;; Put the resulting math.ml file in this directory. When you run 11 | ;; `dune clean`, it will be removed. 12 | (mode 13 | (promote (until-clean))) 14 | ;; We need to give the action access to the files in the `specs` dir. 15 | (deps 16 | (source_tree specs) 17 | ;; We also need to give access to the Python source directory. 18 | (source_tree ../py)) 19 | (action 20 | ;; `pyml_bindgen` outputs directly to stdout, so we need to redirect 21 | ;; the stdout to `math.ml`. We do this using `with-stdout-to`. 22 | (with-stdout-to 23 | ;; This is the name of the file to which the output of the 24 | ;; following command will be directed. 25 | math.ml 26 | ;; This step isn't necessary, but I use `ocamlformat` for 27 | ;; everything, so I would like to have the output of 28 | ;; `pyml_bindgen` also processed by `ocamlformat`. `pipe-stdout` 29 | ;; takes the stdout of the first `run` command and pipes it to the 30 | ;; stdin of the second `run` command. 31 | (pipe-stdout 32 | ;; Here is the acutal `pyml_bindgen` command to generate the 33 | ;; bindings. 34 | (run 35 | %{bin:pyml_bindgen} 36 | specs/adder.txt 37 | adder 38 | Adder 39 | --caml-module 40 | Adder 41 | ;; This option tells `pyml_bindgen` that you want to embed 42 | ;; the Python code into the generated OCaml module. It will 43 | ;; be evaluated at runtime. Note how we refer to the path as 44 | ;; specified in `source_tree` above. 45 | --embed-python-source 46 | ../py/math/adder.py) 47 | ;; And finally, process the bindings code with ocamlformat. 48 | (run ocamlformat --name math.ml -))))) 49 | -------------------------------------------------------------------------------- /examples/embedding_python_source/lib/math.ml: -------------------------------------------------------------------------------- 1 | module Adder : sig 2 | type t 3 | 4 | val of_pyobject : Pytypes.pyobject -> t option 5 | val to_pyobject : t -> Pytypes.pyobject 6 | val add : x:int -> y:int -> unit -> int 7 | end = struct 8 | let filter_opt l = List.filter_map Fun.id l 9 | 10 | let py_module = 11 | lazy 12 | (let source = 13 | {pyml_bindgen_string_literal|class Adder: 14 | @staticmethod 15 | def add(x, y): 16 | return x + y 17 | |pyml_bindgen_string_literal} 18 | in 19 | let filename = 20 | {pyml_bindgen_string_literal|../py/math/adder.py|pyml_bindgen_string_literal} 21 | in 22 | let bytecode = Py.compile ~filename ~source `Exec in 23 | Py.Import.exec_code_module 24 | {pyml_bindgen_string_literal|adder|pyml_bindgen_string_literal} 25 | bytecode) 26 | 27 | let import_module () = Lazy.force py_module 28 | 29 | type t = Pytypes.pyobject 30 | 31 | let is_instance pyo = 32 | let py_class = Py.Module.get (import_module ()) "Adder" in 33 | Py.Object.is_instance pyo py_class 34 | 35 | let of_pyobject pyo = if is_instance pyo then Some pyo else None 36 | let to_pyobject x = x 37 | 38 | let add ~x ~y () = 39 | let class_ = Py.Module.get (import_module ()) "Adder" in 40 | let callable = Py.Object.find_attr_string class_ "add" in 41 | let kwargs = 42 | filter_opt [ Some ("x", Py.Int.of_int x); Some ("y", Py.Int.of_int y) ] 43 | in 44 | Py.Int.to_int @@ Py.Callable.to_function_with_keywords callable [||] kwargs 45 | end 46 | -------------------------------------------------------------------------------- /examples/embedding_python_source/lib/specs/adder.txt: -------------------------------------------------------------------------------- 1 | val add : x:int -> y:int -> unit -> int 2 | -------------------------------------------------------------------------------- /examples/embedding_python_source/py/math/adder.py: -------------------------------------------------------------------------------- 1 | class Adder: 2 | @staticmethod 3 | def add(x, y): 4 | return x + y 5 | -------------------------------------------------------------------------------- /examples/embedding_python_source/run.ml: -------------------------------------------------------------------------------- 1 | open Embedding_py_source_lib.Math 2 | 3 | let () = Py.initialize () 4 | 5 | let result = Adder.add ~x:1 ~y:2 () 6 | 7 | let () = assert (result = 3) 8 | -------------------------------------------------------------------------------- /examples/embedding_python_source/test/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (deps ../run.exe)) 5 | -------------------------------------------------------------------------------- /examples/embedding_python_source/test/run.t: -------------------------------------------------------------------------------- 1 | Run the executable. 2 | 3 | $ ../run.exe 4 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/.ocamlformat: -------------------------------------------------------------------------------- 1 | profile = default 2 | version = 0.23.0 3 | parse-docstrings = true 4 | wrap-comments = true 5 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/README.md: -------------------------------------------------------------------------------- 1 | # Splitting generated modules 2 | 3 | This example shows how you can use `pyml_bindgen` to generate separate `.ml` and `.mli` files for the implementations and signatures of the generated OCaml modules. 4 | 5 | In the `dune` file, you will see two rules, one to generate `ml` and `mli` files for the `Thing` module, and one to generate `ml` and `mli` files for the `Orange` module. 6 | 7 | To do so, you need to provide both the `--caml-module` and `--split-caml-module` options. Something like this: 8 | 9 | ```bash 10 | $ pyml_bindgen specs.txt thing Thing --caml-module Thing --split-caml-module . 11 | ``` 12 | 13 | `--split-caml-module .` says to generate a `thing.ml` and `thing.mli` file in the current directory. You can specify a different directory name to put the generated files in a different directory. E.g., something like `--split-caml-module files/go/here`. 14 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/bin/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (name hello) 5 | (libraries pyml base gen_ml_and_mli_lib)) 6 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/bin/hello.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Gen_ml_and_mli_lib 3 | 4 | let () = Py.initialize () 5 | 6 | let thing = Or_error.ok_exn @@ Thing.create ~name:"Ryan" () 7 | 8 | let () = assert (String.("Ryan" = Thing.name thing)) 9 | 10 | let orange = Or_error.ok_exn @@ Orange.create ~flavor:"Sooo good!" () 11 | 12 | let () = assert (String.("Sooo good!" = Orange.flavor orange)) 13 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | 3 | (cram enable) 4 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (name gen_ml_and_mli_lib) 5 | (libraries pyml base)) 6 | 7 | (rule 8 | (targets thing.ml thing.mli) 9 | (mode 10 | (promote (until-clean))) 11 | (deps 12 | (source_tree specs) 13 | (source_tree py)) 14 | (action 15 | (progn 16 | (run 17 | %{bin:pyml_bindgen} 18 | specs/thing.txt 19 | thing 20 | Thing 21 | --caml-module 22 | Thing 23 | --split-caml-module 24 | . 25 | --of-pyo-ret-type 26 | or_error 27 | --embed-python-source 28 | py/thing.py) 29 | (run ocamlformat thing.ml --inplace) 30 | (run ocamlformat thing.mli --inplace)))) 31 | 32 | (rule 33 | (targets orange.ml orange.mli) 34 | (mode 35 | (promote (until-clean))) 36 | (deps 37 | (source_tree specs) 38 | (source_tree py)) 39 | (action 40 | (progn 41 | (run 42 | %{bin:pyml_bindgen} 43 | specs/orange.txt 44 | orange 45 | Orange 46 | --caml-module 47 | Orange 48 | --split-caml-module 49 | . 50 | --of-pyo-ret-type 51 | or_error 52 | --embed-python-source 53 | py/orange.py) 54 | (run ocamlformat orange.ml --inplace) 55 | (run ocamlformat orange.mli --inplace)))) 56 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/lib/orange.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | 3 | let filter_opt = List.filter_opt 4 | 5 | let py_module = 6 | lazy 7 | (let source = 8 | {pyml_bindgen_string_literal|class Orange: 9 | def __init__(self, flavor): 10 | self.flavor = flavor 11 | |pyml_bindgen_string_literal} 12 | in 13 | let filename = 14 | {pyml_bindgen_string_literal|py/orange.py|pyml_bindgen_string_literal} 15 | in 16 | let bytecode = Py.compile ~filename ~source `Exec in 17 | Py.Import.exec_code_module 18 | {pyml_bindgen_string_literal|orange|pyml_bindgen_string_literal} bytecode) 19 | 20 | let import_module () = Lazy.force py_module 21 | 22 | type t = Pytypes.pyobject 23 | 24 | let is_instance pyo = 25 | let py_class = Py.Module.get (import_module ()) "Orange" in 26 | Py.Object.is_instance pyo py_class 27 | 28 | let of_pyobject pyo = 29 | if is_instance pyo then Or_error.return pyo 30 | else Or_error.error_string "Expected Orange" 31 | 32 | let to_pyobject x = x 33 | 34 | let create ~flavor () = 35 | let callable = Py.Module.get (import_module ()) "Orange" in 36 | let kwargs = filter_opt [ Some ("flavor", Py.String.of_string flavor) ] in 37 | of_pyobject @@ Py.Callable.to_function_with_keywords callable [||] kwargs 38 | 39 | let flavor t = Py.String.to_string @@ Py.Object.find_attr_string t "flavor" 40 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/lib/orange.mli: -------------------------------------------------------------------------------- 1 | open! Base 2 | 3 | type t 4 | 5 | val of_pyobject : Pytypes.pyobject -> t Or_error.t 6 | val to_pyobject : t -> Pytypes.pyobject 7 | val create : flavor:string -> unit -> t Or_error.t 8 | val flavor : t -> string 9 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/lib/py/orange.py: -------------------------------------------------------------------------------- 1 | class Orange: 2 | def __init__(self, flavor): 3 | self.flavor = flavor 4 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/lib/py/thing.py: -------------------------------------------------------------------------------- 1 | class Thing: 2 | def __init__(self, name): 3 | self.name = name 4 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/lib/specs/orange.txt: -------------------------------------------------------------------------------- 1 | val create : flavor:string -> unit -> t Or_error.t 2 | [@@py_fun_name __init__] 3 | 4 | val flavor : t -> string 5 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/lib/specs/thing.txt: -------------------------------------------------------------------------------- 1 | val create : name:string -> unit -> t Or_error.t 2 | [@@py_fun_name __init__] 3 | 4 | val name : t -> string 5 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/lib/thing.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | 3 | let filter_opt = List.filter_opt 4 | 5 | let py_module = 6 | lazy 7 | (let source = 8 | {pyml_bindgen_string_literal|class Thing: 9 | def __init__(self, name): 10 | self.name = name 11 | |pyml_bindgen_string_literal} 12 | in 13 | let filename = 14 | {pyml_bindgen_string_literal|py/thing.py|pyml_bindgen_string_literal} 15 | in 16 | let bytecode = Py.compile ~filename ~source `Exec in 17 | Py.Import.exec_code_module 18 | {pyml_bindgen_string_literal|thing|pyml_bindgen_string_literal} bytecode) 19 | 20 | let import_module () = Lazy.force py_module 21 | 22 | type t = Pytypes.pyobject 23 | 24 | let is_instance pyo = 25 | let py_class = Py.Module.get (import_module ()) "Thing" in 26 | Py.Object.is_instance pyo py_class 27 | 28 | let of_pyobject pyo = 29 | if is_instance pyo then Or_error.return pyo 30 | else Or_error.error_string "Expected Thing" 31 | 32 | let to_pyobject x = x 33 | 34 | let create ~name () = 35 | let callable = Py.Module.get (import_module ()) "Thing" in 36 | let kwargs = filter_opt [ Some ("name", Py.String.of_string name) ] in 37 | of_pyobject @@ Py.Callable.to_function_with_keywords callable [||] kwargs 38 | 39 | let name t = Py.String.to_string @@ Py.Object.find_attr_string t "name" 40 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/lib/thing.mli: -------------------------------------------------------------------------------- 1 | open! Base 2 | 3 | type t 4 | 5 | val of_pyobject : Pytypes.pyobject -> t Or_error.t 6 | val to_pyobject : t -> Pytypes.pyobject 7 | val create : name:string -> unit -> t Or_error.t 8 | val name : t -> string 9 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/test/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (deps ../bin/hello.exe)) 5 | -------------------------------------------------------------------------------- /examples/gen_ml_and_mli/test/it_works.t: -------------------------------------------------------------------------------- 1 | Check it 2 | 3 | $ ../bin/hello.exe 4 | -------------------------------------------------------------------------------- /examples/importing_modules/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (name run) 5 | (libraries pyml importing_modules_lib)) 6 | -------------------------------------------------------------------------------- /examples/importing_modules/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | -------------------------------------------------------------------------------- /examples/importing_modules/lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (name importing_modules_lib) 5 | (libraries pyml)) 6 | 7 | (rule 8 | ;; The file produced by this rule will be called `lib.ml`. 9 | (target magic_dust.ml) 10 | ;; Put the resulting lib.ml file in this directory. When you run 11 | ;; `dune clean`, it will be removed. 12 | (mode 13 | (promote (until-clean))) 14 | ;; We need to give the action access to the files in the `specs` dir. 15 | (deps 16 | (source_tree specs)) 17 | (action 18 | ;; We use `progn` to run multiple commands one after the other. 19 | (progn 20 | ;; `with-stdout-to` will collect all standard output of anything 21 | ;; inside of it. 22 | (with-stdout-to 23 | ;; Output of commands in this sexp will be collected in `magic_dust.ml`. 24 | magic_dust.ml 25 | ;; We use `progn` again to run mulitple commands. The output of 26 | ;; both will go into `magic_dust.ml`. 27 | (progn 28 | ;; The first time we run `pyml_bindgen` to generate the `Hearts` 29 | ;; module. 30 | (run 31 | %{bin:pyml_bindgen} 32 | ./specs/magic_dust/hearts.txt 33 | ;; The python module is in `./py/magic_dust/hearts.py`. See 34 | ;; the README for why we specify the module like this. 35 | magic_dust.hearts 36 | NA 37 | --caml-module 38 | Hearts 39 | --associated-with 40 | module) 41 | ;; We run `pyml_bindgen` again to generate the `Sparkles` module. 42 | ;; Both of these will be in the `Magic_dust` OCaml module. 43 | (run 44 | %{bin:pyml_bindgen} 45 | ./specs/magic_dust/sparkles.txt 46 | magic_dust.sparkles 47 | NA 48 | --caml-module 49 | Sparkles 50 | --associated-with 51 | module))) 52 | ;; Finally we format the `magic_dust.ml` file so it looks nice. 53 | (run ocamlformat magic_dust.ml --inplace)))) 54 | 55 | ;; This rule is similar to the previous, though it generates the 56 | ;; `Silly_math` OCaml module. 57 | 58 | (rule 59 | (target silly_math.ml) 60 | (mode 61 | (promote (until-clean))) 62 | (deps 63 | (source_tree specs)) 64 | (action 65 | (progn 66 | (with-stdout-to 67 | silly_math.ml 68 | (progn 69 | (run 70 | %{bin:pyml_bindgen} 71 | specs/silly_math/adder/add.txt 72 | silly_math.adder.add 73 | NA 74 | --caml-module 75 | Add 76 | --associated-with 77 | module) 78 | (run 79 | %{bin:pyml_bindgen} 80 | specs/silly_math/subtracter/subtract.txt 81 | silly_math.subtracter.subtract 82 | NA 83 | --caml-module 84 | Subtract 85 | --associated-with 86 | module))) 87 | (run ocamlformat silly_math.ml --inplace)))) 88 | -------------------------------------------------------------------------------- /examples/importing_modules/lib/magic_dust.ml: -------------------------------------------------------------------------------- 1 | module Hearts : sig 2 | type t 3 | 4 | val of_pyobject : Pytypes.pyobject -> t option 5 | val to_pyobject : t -> Pytypes.pyobject 6 | val hearts : unit -> string 7 | end = struct 8 | let filter_opt l = List.filter_map Fun.id l 9 | let py_module = lazy (Py.Import.import_module "magic_dust.hearts") 10 | let import_module () = Lazy.force py_module 11 | 12 | type t = Pytypes.pyobject 13 | 14 | let is_instance pyo = 15 | let py_class = Py.Module.get (import_module ()) "NA" in 16 | Py.Object.is_instance pyo py_class 17 | 18 | let of_pyobject pyo = if is_instance pyo then Some pyo else None 19 | let to_pyobject x = x 20 | 21 | let hearts () = 22 | let callable = Py.Module.get (import_module ()) "hearts" in 23 | let kwargs = filter_opt [] in 24 | Py.String.to_string 25 | @@ Py.Callable.to_function_with_keywords callable [||] kwargs 26 | end 27 | 28 | module Sparkles : sig 29 | type t 30 | 31 | val of_pyobject : Pytypes.pyobject -> t option 32 | val to_pyobject : t -> Pytypes.pyobject 33 | val sparkles : unit -> string 34 | end = struct 35 | let filter_opt l = List.filter_map Fun.id l 36 | let py_module = lazy (Py.Import.import_module "magic_dust.sparkles") 37 | let import_module () = Lazy.force py_module 38 | 39 | type t = Pytypes.pyobject 40 | 41 | let is_instance pyo = 42 | let py_class = Py.Module.get (import_module ()) "NA" in 43 | Py.Object.is_instance pyo py_class 44 | 45 | let of_pyobject pyo = if is_instance pyo then Some pyo else None 46 | let to_pyobject x = x 47 | 48 | let sparkles () = 49 | let callable = Py.Module.get (import_module ()) "sparkles" in 50 | let kwargs = filter_opt [] in 51 | Py.String.to_string 52 | @@ Py.Callable.to_function_with_keywords callable [||] kwargs 53 | end 54 | -------------------------------------------------------------------------------- /examples/importing_modules/lib/silly_math.ml: -------------------------------------------------------------------------------- 1 | module Add : sig 2 | type t 3 | 4 | val of_pyobject : Pytypes.pyobject -> t option 5 | val to_pyobject : t -> Pytypes.pyobject 6 | val add : x:int -> y:int -> unit -> int 7 | end = struct 8 | let filter_opt l = List.filter_map Fun.id l 9 | let py_module = lazy (Py.Import.import_module "silly_math.adder.add") 10 | let import_module () = Lazy.force py_module 11 | 12 | type t = Pytypes.pyobject 13 | 14 | let is_instance pyo = 15 | let py_class = Py.Module.get (import_module ()) "NA" in 16 | Py.Object.is_instance pyo py_class 17 | 18 | let of_pyobject pyo = if is_instance pyo then Some pyo else None 19 | let to_pyobject x = x 20 | 21 | let add ~x ~y () = 22 | let callable = Py.Module.get (import_module ()) "add" in 23 | let kwargs = 24 | filter_opt [ Some ("x", Py.Int.of_int x); Some ("y", Py.Int.of_int y) ] 25 | in 26 | Py.Int.to_int @@ Py.Callable.to_function_with_keywords callable [||] kwargs 27 | end 28 | 29 | module Subtract : sig 30 | type t 31 | 32 | val of_pyobject : Pytypes.pyobject -> t option 33 | val to_pyobject : t -> Pytypes.pyobject 34 | val subtract : x:int -> y:int -> unit -> int 35 | end = struct 36 | let filter_opt l = List.filter_map Fun.id l 37 | 38 | let py_module = 39 | lazy (Py.Import.import_module "silly_math.subtracter.subtract") 40 | 41 | let import_module () = Lazy.force py_module 42 | 43 | type t = Pytypes.pyobject 44 | 45 | let is_instance pyo = 46 | let py_class = Py.Module.get (import_module ()) "NA" in 47 | Py.Object.is_instance pyo py_class 48 | 49 | let of_pyobject pyo = if is_instance pyo then Some pyo else None 50 | let to_pyobject x = x 51 | 52 | let subtract ~x ~y () = 53 | let callable = Py.Module.get (import_module ()) "subtract" in 54 | let kwargs = 55 | filter_opt [ Some ("x", Py.Int.of_int x); Some ("y", Py.Int.of_int y) ] 56 | in 57 | Py.Int.to_int @@ Py.Callable.to_function_with_keywords callable [||] kwargs 58 | end 59 | -------------------------------------------------------------------------------- /examples/importing_modules/lib/specs/magic_dust/hearts.txt: -------------------------------------------------------------------------------- 1 | val hearts : unit -> string 2 | 3 | -------------------------------------------------------------------------------- /examples/importing_modules/lib/specs/magic_dust/sparkles.txt: -------------------------------------------------------------------------------- 1 | val sparkles : unit -> string 2 | -------------------------------------------------------------------------------- /examples/importing_modules/lib/specs/silly_math/adder/add.txt: -------------------------------------------------------------------------------- 1 | val add : x:int -> y:int -> unit -> int 2 | -------------------------------------------------------------------------------- /examples/importing_modules/lib/specs/silly_math/subtracter/subtract.txt: -------------------------------------------------------------------------------- 1 | val subtract : x:int -> y:int -> unit -> int 2 | -------------------------------------------------------------------------------- /examples/importing_modules/py/magic_dust/hearts.py: -------------------------------------------------------------------------------- 1 | def hearts(): 2 | return('hearts...') 3 | -------------------------------------------------------------------------------- /examples/importing_modules/py/magic_dust/sparkles.py: -------------------------------------------------------------------------------- 1 | def sparkles(): 2 | return('sparkle, sparkle!!') 3 | -------------------------------------------------------------------------------- /examples/importing_modules/py/silly_math/adder/add.py: -------------------------------------------------------------------------------- 1 | def add(x, y): 2 | return x + y 3 | -------------------------------------------------------------------------------- /examples/importing_modules/py/silly_math/subtracter/subtract.py: -------------------------------------------------------------------------------- 1 | def subtract(x, y): 2 | return x - y 3 | -------------------------------------------------------------------------------- /examples/importing_modules/run.ml: -------------------------------------------------------------------------------- 1 | open Importing_modules_lib 2 | 3 | let () = Py.initialize () 4 | 5 | let () = assert (3 = Silly_math.Add.add ~x:1 ~y:2 ()) 6 | 7 | let () = assert (-1 = Silly_math.Subtract.subtract ~x:1 ~y:2 ()) 8 | 9 | let () = assert ("sparkle, sparkle!!" = Magic_dust.Sparkles.sparkles ()) 10 | 11 | let () = assert ("hearts..." = Magic_dust.Hearts.hearts ()) 12 | -------------------------------------------------------------------------------- /examples/importing_modules/test/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (deps 5 | ../run.exe 6 | (source_tree ../py))) 7 | -------------------------------------------------------------------------------- /examples/importing_modules/test/run.t: -------------------------------------------------------------------------------- 1 | Run the executable. 2 | 3 | $ PYTHONPATH=$(pwd)/../py ../run.exe 4 | -------------------------------------------------------------------------------- /examples/quick_start/.ocamlformat: -------------------------------------------------------------------------------- 1 | profile = default 2 | version = 0.23.0 3 | parse-docstrings = true 4 | wrap-comments = true 5 | -------------------------------------------------------------------------------- /examples/quick_start/README.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | This is a simple example where all files including the Python module that we want to bind are in a single directory. 4 | 5 | For more details about this example, see the [Quick Start](https://github.com/mooreryan/ocaml_python_bindgen#quick-start). There you will see an explanation of how to run `pyml_bindgen` to generate the bindings you see here. 6 | 7 | ## Files 8 | 9 | * `adder.py`: the `Adder` class that we want to bind 10 | * `val_specs.txt`: the OCaml value specifications that define the binding functions 11 | * `lib.ml`: library file generated by running `pyml_bindgen` 12 | * `run.ml`: a runner script for driving the example 13 | * `dune`: the dune file which includes a `rule` for auto-generating the `lib.ml` file 14 | 15 | ## Generating `lib.ml` 16 | 17 | `lib.ml` is the file of `pyml` bindings generated by `pyml_bindgen`. 18 | 19 | The `dune` file has a rule to automatically generate this file for you when you run `dune build` in the root of the repository. Check out the `dune` file to see how to do this...it has lots of comments in there! 20 | 21 | However, if you wanted to make this file manually, you could do so like this: 22 | 23 | ``` 24 | $ pyml_bindgen val_specs.txt adder Adder --caml-module Adder | \ 25 | ocamlformat --name lib.ml - 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/quick_start/adder.py: -------------------------------------------------------------------------------- 1 | class Adder: 2 | @staticmethod 3 | def add(x, y): 4 | return x + y 5 | -------------------------------------------------------------------------------- /examples/quick_start/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (name run) 5 | (libraries pyml)) 6 | 7 | (rule 8 | ;; The file produced by this rule will be called `lib.ml`. 9 | (target lib.ml) 10 | ;; Put the resulting lib.ml file in this directory. When you run 11 | ;; `dune clean`, it will be removed. 12 | (mode 13 | (promote (until-clean))) 14 | ;; Need this expclicitly with dune 3 15 | (deps 16 | (:val_specs val_specs.txt)) 17 | (action 18 | ;; `pyml_bindgen` outputs directly to stdout, so we need to redirect 19 | ;; the stdout to `lib.ml`. We do this using `with-stdout-to`. 20 | (with-stdout-to 21 | ;; This is the name of the file to which the output of the 22 | ;; following command will be directed. 23 | lib.ml 24 | ;; This step isn't necessary, but I use `ocamlformat` for 25 | ;; everything, so I would like to have the output of 26 | ;; `pyml_bindgen` also processed by `ocamlformat`. `pipe-stdout` 27 | ;; takes the stdout of the first `run` command and pipes it to the 28 | ;; stdin of the second `run` command. 29 | (pipe-stdout 30 | ;; Here is the acutal `pyml_bindgen` command to generate the 31 | ;; bindings. 32 | (run %{bin:pyml_bindgen} %{val_specs} adder Adder --caml-module Adder) 33 | ;; And finally, process the bindings code with ocamlformat. 34 | (run ocamlformat --name lib.ml -))))) 35 | -------------------------------------------------------------------------------- /examples/quick_start/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | -------------------------------------------------------------------------------- /examples/quick_start/lib.ml: -------------------------------------------------------------------------------- 1 | module Adder : sig 2 | type t 3 | 4 | val of_pyobject : Pytypes.pyobject -> t option 5 | val to_pyobject : t -> Pytypes.pyobject 6 | val add : x:int -> y:int -> unit -> int 7 | end = struct 8 | let filter_opt l = List.filter_map Fun.id l 9 | let py_module = lazy (Py.Import.import_module "adder") 10 | let import_module () = Lazy.force py_module 11 | 12 | type t = Pytypes.pyobject 13 | 14 | let is_instance pyo = 15 | let py_class = Py.Module.get (import_module ()) "Adder" in 16 | Py.Object.is_instance pyo py_class 17 | 18 | let of_pyobject pyo = if is_instance pyo then Some pyo else None 19 | let to_pyobject x = x 20 | 21 | let add ~x ~y () = 22 | let class_ = Py.Module.get (import_module ()) "Adder" in 23 | let callable = Py.Object.find_attr_string class_ "add" in 24 | let kwargs = 25 | filter_opt [ Some ("x", Py.Int.of_int x); Some ("y", Py.Int.of_int y) ] 26 | in 27 | Py.Int.to_int @@ Py.Callable.to_function_with_keywords callable [||] kwargs 28 | end 29 | -------------------------------------------------------------------------------- /examples/quick_start/run.ml: -------------------------------------------------------------------------------- 1 | open Lib 2 | 3 | let () = Py.initialize () 4 | 5 | let result = Adder.add ~x:1 ~y:2 () 6 | 7 | let () = assert (result = 3) 8 | -------------------------------------------------------------------------------- /examples/quick_start/test/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (deps 5 | ../run.exe 6 | (source_tree ..))) 7 | -------------------------------------------------------------------------------- /examples/quick_start/test/run.t: -------------------------------------------------------------------------------- 1 | Run the executable. 2 | 3 | $ PYTHONPATH=$(pwd)/.. ../run.exe 4 | -------------------------------------------------------------------------------- /examples/quick_start/val_specs.txt: -------------------------------------------------------------------------------- 1 | val add : x:int -> y:int -> unit -> int 2 | -------------------------------------------------------------------------------- /examples/recursive_modules/.ocamlformat: -------------------------------------------------------------------------------- 1 | profile = default 2 | version = 0.23.0 3 | parse-docstrings = true 4 | wrap-comments = true 5 | -------------------------------------------------------------------------------- /examples/recursive_modules/README.md: -------------------------------------------------------------------------------- 1 | # Recursive Modules 2 | 3 | When writing bindings for Python libraries, you will need to bind cyclic Python classes, or, Python classes that reference each other. Here is an example. 4 | 5 | ```python 6 | class Cat: 7 | # Cats have humans 8 | def __init__(self, name): 9 | self.name = name 10 | self.human = None 11 | 12 | def __str__(self): 13 | if self.human: 14 | human = self.human.name 15 | else: 16 | human = "none" 17 | 18 | return f'Cat -- name: {self.name}, human: {human}' 19 | 20 | def adopt_human(self, human): 21 | self.human = human 22 | 23 | class Human: 24 | # Humans have cats 25 | def __init__(self, name): 26 | self.name = name 27 | self.cat = None 28 | 29 | def __str__(self): 30 | if self.cat: 31 | cat = self.cat.name 32 | else: 33 | cat = "none" 34 | 35 | return f'Human -- name: {self.name}, cat: {cat}' 36 | 37 | def adopt_cat(self, cat): 38 | self.cat = cat 39 | ``` 40 | 41 | You can see that the `Cat` and `Human` classes make reference to one another. This can be a problem when writing OCaml bindings. 42 | 43 | One solution is to use [recursive modules](https://ocaml.org/manual/recursivemodules.html). Rather than run `pyml_bindgen` separately for each class we want to bind, and then manually [modify](https://mooreryan.github.io/ocaml_python_bindgen/recursive/) the results to make the modules recursive, this directory shows you how you can use the helper scripts `gen_multi` and `combine_rec_modules` to automate this task. 44 | 45 | ## `gen_multi` 46 | 47 | `gen_multi` is a wrapper for running `pyml_bindgen` multiples times and combining it's output into a single file. 48 | 49 | It takes a tab-separated file (TSV), where each line describes the command line options for running `pyml_bindgen` on a specific specs file. 50 | 51 | Here is what the input file `cli_specs.tsv` looks like from this example. 52 | 53 | | signatures | py_module | py_class | associated_with | caml_module | split_caml_module | embed_python_source | of_pyo_ret_type | 54 | |----------------------|-----------|----------|-----------------|-------------|-------------------|---------------------|-----------------| 55 | | specs/human_spec.txt | human | Human | class | Human | NA | ../py/human.py | no_check | 56 | | specs/cat_spec.txt | cat | Cat | class | Cat | NA | ../py/cat.py | no_check | 57 | 58 | The first row must be given in this exact order. Each column is one of the command line options to `pyml_bindgen`. You can put `NA` (or `na`, or blank) in cases in which you would not pass the flag/option to `pyml_bindgen`. 59 | 60 | One potentially tricky thing is that the file paths will be with respect to the location in which the `gen_multi` program is run from, rather than the location of the `cli_specs.tsv` file. If it is giving you trouble, you could use absolute paths instead. 61 | 62 | ## `combine_rec_modules` 63 | 64 | This is a simple script that takes the output of say, `gen_multi` and turns all of the modules into recursive modules. 65 | 66 | Something like this 67 | 68 | ```ocaml 69 | module A : sig 70 | ... 71 | end = struct 72 | ... 73 | end 74 | 75 | module B : sig 76 | ... 77 | end = struct 78 | ... 79 | end 80 | 81 | module C : sig 82 | ... 83 | end = struct 84 | ... 85 | end 86 | ``` 87 | 88 | would become something like this: 89 | 90 | ```ocaml 91 | module rec A : sig 92 | ... 93 | end = struct 94 | ... 95 | end 96 | 97 | and B : sig 98 | ... 99 | end = struct 100 | ... 101 | end 102 | 103 | and C : sig 104 | ... 105 | end = struct 106 | ... 107 | end 108 | ``` 109 | 110 | ## Generating the modules 111 | 112 | See `lib/dune` for an automatic way to do this. But basically, it goes something like this: 113 | 114 | ```bash 115 | $ gen_multi ./specs/cli_specs.tsv \ 116 | | combine_rec_modules /dev/stdin \ 117 | | ocamlformat --name a.ml - \ 118 | > lib.ml 119 | ``` 120 | -------------------------------------------------------------------------------- /examples/recursive_modules/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (name run) 5 | (libraries base pyml stdio examples_recursive_modules_lib)) 6 | -------------------------------------------------------------------------------- /examples/recursive_modules/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | -------------------------------------------------------------------------------- /examples/recursive_modules/lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (name examples_recursive_modules_lib) 5 | (libraries pyml)) 6 | 7 | (rule 8 | (target lib.ml) 9 | (mode 10 | (promote (until-clean))) 11 | (deps 12 | (source_tree specs) 13 | (source_tree ../py)) 14 | (action 15 | (with-stdout-to 16 | lib.ml 17 | (pipe-stdout 18 | (run %{bin:gen_multi} ./specs/cli_specs.tsv) 19 | (run %{bin:combine_rec_modules} /dev/stdin) 20 | (run ocamlformat --name a.ml -))))) 21 | -------------------------------------------------------------------------------- /examples/recursive_modules/lib/lib.ml: -------------------------------------------------------------------------------- 1 | module rec Human : sig 2 | type t 3 | 4 | val of_pyobject : Pytypes.pyobject -> t 5 | val to_pyobject : t -> Pytypes.pyobject 6 | val create : name:string -> unit -> t 7 | val to_string : t -> unit -> string 8 | val adopt_cat : t -> cat:Cat.t -> unit -> unit 9 | val name : t -> string 10 | val cat : t -> Cat.t 11 | end = struct 12 | let filter_opt l = List.filter_map Fun.id l 13 | 14 | let py_module = 15 | lazy 16 | (let source = 17 | {pyml_bindgen_string_literal|class Human: 18 | # Humans have cats 19 | def __init__(self, name): 20 | self.name = name 21 | self.cat = None 22 | 23 | def __str__(self): 24 | if self.cat: 25 | cat = self.cat.name 26 | else: 27 | cat = "none" 28 | 29 | return f'Human -- name: {self.name}, cat: {cat}' 30 | 31 | def adopt_cat(self, cat): 32 | self.cat = cat 33 | |pyml_bindgen_string_literal} 34 | in 35 | let filename = 36 | {pyml_bindgen_string_literal|../py/human.py|pyml_bindgen_string_literal} 37 | in 38 | let bytecode = Py.compile ~filename ~source `Exec in 39 | Py.Import.exec_code_module 40 | {pyml_bindgen_string_literal|human|pyml_bindgen_string_literal} 41 | bytecode) 42 | 43 | let import_module () = Lazy.force py_module 44 | 45 | type t = Pytypes.pyobject 46 | 47 | let of_pyobject pyo = pyo 48 | let to_pyobject x = x 49 | 50 | let create ~name () = 51 | let callable = Py.Module.get (import_module ()) "Human" in 52 | let kwargs = filter_opt [ Some ("name", Py.String.of_string name) ] in 53 | of_pyobject @@ Py.Callable.to_function_with_keywords callable [||] kwargs 54 | 55 | let to_string t () = 56 | let callable = Py.Object.find_attr_string t "__str__" in 57 | let kwargs = filter_opt [] in 58 | Py.String.to_string 59 | @@ Py.Callable.to_function_with_keywords callable [||] kwargs 60 | 61 | let adopt_cat t ~cat () = 62 | let callable = Py.Object.find_attr_string t "adopt_cat" in 63 | let kwargs = filter_opt [ Some ("cat", Cat.to_pyobject cat) ] in 64 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 65 | 66 | let name t = Py.String.to_string @@ Py.Object.find_attr_string t "name" 67 | let cat t = Cat.of_pyobject @@ Py.Object.find_attr_string t "cat" 68 | end 69 | 70 | and Cat : sig 71 | type t 72 | 73 | val of_pyobject : Pytypes.pyobject -> t 74 | val to_pyobject : t -> Pytypes.pyobject 75 | val create : name:string -> unit -> t 76 | val to_string : t -> unit -> string 77 | val adopt_human : t -> human:Human.t -> unit -> unit 78 | val name : t -> string 79 | val human : t -> Human.t 80 | end = struct 81 | let filter_opt l = List.filter_map Fun.id l 82 | 83 | let py_module = 84 | lazy 85 | (let source = 86 | {pyml_bindgen_string_literal|class Cat: 87 | # Cats have humans 88 | def __init__(self, name): 89 | self.name = name 90 | self.human = None 91 | 92 | def __str__(self): 93 | if self.human: 94 | human = self.human.name 95 | else: 96 | human = "none" 97 | 98 | return f'Cat -- name: {self.name}, human: {human}' 99 | 100 | def adopt_human(self, human): 101 | self.human = human 102 | |pyml_bindgen_string_literal} 103 | in 104 | let filename = 105 | {pyml_bindgen_string_literal|../py/cat.py|pyml_bindgen_string_literal} 106 | in 107 | let bytecode = Py.compile ~filename ~source `Exec in 108 | Py.Import.exec_code_module 109 | {pyml_bindgen_string_literal|cat|pyml_bindgen_string_literal} bytecode) 110 | 111 | let import_module () = Lazy.force py_module 112 | 113 | type t = Pytypes.pyobject 114 | 115 | let of_pyobject pyo = pyo 116 | let to_pyobject x = x 117 | 118 | let create ~name () = 119 | let callable = Py.Module.get (import_module ()) "Cat" in 120 | let kwargs = filter_opt [ Some ("name", Py.String.of_string name) ] in 121 | of_pyobject @@ Py.Callable.to_function_with_keywords callable [||] kwargs 122 | 123 | let to_string t () = 124 | let callable = Py.Object.find_attr_string t "__str__" in 125 | let kwargs = filter_opt [] in 126 | Py.String.to_string 127 | @@ Py.Callable.to_function_with_keywords callable [||] kwargs 128 | 129 | let adopt_human t ~human () = 130 | let callable = Py.Object.find_attr_string t "adopt_human" in 131 | let kwargs = filter_opt [ Some ("human", Human.to_pyobject human) ] in 132 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 133 | 134 | let name t = Py.String.to_string @@ Py.Object.find_attr_string t "name" 135 | let human t = Human.of_pyobject @@ Py.Object.find_attr_string t "human" 136 | end 137 | -------------------------------------------------------------------------------- /examples/recursive_modules/lib/specs/cat_spec.txt: -------------------------------------------------------------------------------- 1 | val create : name:string -> unit -> t 2 | [@@py_fun_name __init__] 3 | 4 | val to_string : t -> unit -> string 5 | [@@py_fun_name __str__] 6 | 7 | val adopt_human : t -> human:Human.t -> unit -> unit 8 | 9 | val name : t -> string 10 | val human : t -> Human.t -------------------------------------------------------------------------------- /examples/recursive_modules/lib/specs/cli_specs.tsv: -------------------------------------------------------------------------------- 1 | signatures py_module py_class associated_with caml_module split_caml_module embed_python_source of_pyo_ret_type 2 | specs/human_spec.txt human Human class Human NA ../py/human.py no_check 3 | specs/cat_spec.txt cat Cat class Cat NA ../py/cat.py no_check 4 | -------------------------------------------------------------------------------- /examples/recursive_modules/lib/specs/human_spec.txt: -------------------------------------------------------------------------------- 1 | val create : name:string -> unit -> t 2 | [@@py_fun_name __init__] 3 | 4 | val to_string : t -> unit -> string 5 | [@@py_fun_name __str__] 6 | 7 | val adopt_cat : t -> cat:Cat.t -> unit -> unit 8 | 9 | val name : t -> string 10 | val cat : t -> Cat.t -------------------------------------------------------------------------------- /examples/recursive_modules/py/cat.py: -------------------------------------------------------------------------------- 1 | class Cat: 2 | # Cats have humans 3 | def __init__(self, name): 4 | self.name = name 5 | self.human = None 6 | 7 | def __str__(self): 8 | if self.human: 9 | human = self.human.name 10 | else: 11 | human = "none" 12 | 13 | return f'Cat -- name: {self.name}, human: {human}' 14 | 15 | def adopt_human(self, human): 16 | self.human = human 17 | -------------------------------------------------------------------------------- /examples/recursive_modules/py/human.py: -------------------------------------------------------------------------------- 1 | class Human: 2 | # Humans have cats 3 | def __init__(self, name): 4 | self.name = name 5 | self.cat = None 6 | 7 | def __str__(self): 8 | if self.cat: 9 | cat = self.cat.name 10 | else: 11 | cat = "none" 12 | 13 | return f'Human -- name: {self.name}, cat: {cat}' 14 | 15 | def adopt_cat(self, cat): 16 | self.cat = cat 17 | -------------------------------------------------------------------------------- /examples/recursive_modules/run.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Examples_recursive_modules_lib.Lib 3 | 4 | let () = Py.initialize () 5 | 6 | let print_endline = Stdio.print_endline 7 | 8 | let human = Human.create ~name:"Bob" () 9 | 10 | let cat = Cat.create ~name:"Apple" () 11 | 12 | let () = 13 | print_endline "Before adoption..."; 14 | print_endline @@ Human.to_string human (); 15 | print_endline @@ Cat.to_string cat () 16 | 17 | let () = 18 | Human.adopt_cat human ~cat (); 19 | Cat.adopt_human cat ~human () 20 | 21 | let () = 22 | print_endline "After adoption..."; 23 | print_endline @@ Human.to_string human (); 24 | print_endline @@ Cat.to_string cat () 25 | -------------------------------------------------------------------------------- /examples/recursive_modules/test/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (deps ../run.exe)) 5 | -------------------------------------------------------------------------------- /examples/recursive_modules/test/run.t: -------------------------------------------------------------------------------- 1 | Run the executable. 2 | 3 | $ ../run.exe 4 | Before adoption... 5 | Human -- name: Bob, cat: none 6 | Cat -- name: Apple, human: none 7 | After adoption... 8 | Human -- name: Bob, cat: Apple 9 | Cat -- name: Apple, human: Bob 10 | -------------------------------------------------------------------------------- /examples/tuples/.ocamlformat: -------------------------------------------------------------------------------- 1 | profile = default 2 | version = 0.23.0 3 | parse-docstrings = true 4 | wrap-comments = true 5 | -------------------------------------------------------------------------------- /examples/tuples/README.md: -------------------------------------------------------------------------------- 1 | # Tuples 2 | 3 | - They can be passed in as arguments, or returned from functions. 4 | - Only basic types and python objects can occur in tuples 5 | - No nesting 6 | - No options 7 | - You _can_ stick tuples inside a `List`, `Seq`, or `Array`, e.g., `(int * string) list`, but _not_ in `option` or `Or_error.t`. 8 | - If you break these rules, you will get runtime errors :) 9 | 10 | ## Generate bindings 11 | 12 | See the `dune` file for auto-generating the bindings. If you don't want to use Dune rules for this, you can still use the rule as an example of how to run `pyml_bindgen` manually. 13 | -------------------------------------------------------------------------------- /examples/tuples/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (name run) 5 | (libraries pyml)) 6 | 7 | (rule 8 | ;; The file produced by this rule will be called `lib.ml`. 9 | (target lib.ml) 10 | ;; Put the resulting lib.ml file in this directory. When you run 11 | ;; `dune clean`, it will be removed. 12 | (mode 13 | (promote (until-clean))) 14 | ;; Need this expclicitly with dune 3 15 | (deps 16 | (:val_specs val_specs.txt)) 17 | (action 18 | ;; `pyml_bindgen` outputs directly to stdout, so we need to redirect 19 | ;; the stdout to `lib.ml`. We do this using `with-stdout-to`. 20 | (with-stdout-to 21 | ;; This is the name of the file to which the output of the 22 | ;; following command will be directed. 23 | lib.ml 24 | ;; This step isn't necessary, but I use `ocamlformat` for 25 | ;; everything, so I would like to have the output of 26 | ;; `pyml_bindgen` also processed by `ocamlformat`. `pipe-stdout` 27 | ;; takes the stdout of the first `run` command and pipes it to the 28 | ;; stdin of the second `run` command. 29 | (pipe-stdout 30 | ;; Here is the acutal `pyml_bindgen` command to generate the 31 | ;; bindings. 32 | (run 33 | %{bin:pyml_bindgen} 34 | %{val_specs} 35 | tuples 36 | NA 37 | --associated-with 38 | module 39 | --caml-module 40 | Tuples) 41 | ;; And finally, process the bindings code with ocamlformat. 42 | (run ocamlformat --name lib.ml -))))) 43 | -------------------------------------------------------------------------------- /examples/tuples/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | -------------------------------------------------------------------------------- /examples/tuples/run.ml: -------------------------------------------------------------------------------- 1 | open Lib 2 | 3 | let () = Py.initialize () 4 | 5 | let () = assert (Tuples.pair ~x:1 ~y:"apple" () = (1, "apple")) 6 | 7 | let () = assert (Tuples.first ~x:(1, 2) () = 1) 8 | 9 | let () = assert (Tuples.identity ~x:(1, 2) () = (1, 2)) 10 | 11 | let () = assert (Tuples.make () = (0, 0)) 12 | 13 | let () = assert (Tuples.apple ~x:[ 1; 2 ] () = [ 1; 2 ]) 14 | 15 | let () = assert (Tuples.pie_list ~x:[ (1, 2); (3, 4) ] () = [ (1, 2); (3, 4) ]) 16 | 17 | let () = 18 | assert (Tuples.pie_array ~x:[| (1, 2); (3, 4) |] () = [| (1, 2); (3, 4) |]) 19 | 20 | let () = 21 | let l = [ (1, 2); (3, 4) ] in 22 | let x = List.to_seq l in 23 | let result = Tuples.pie_seq ~x () in 24 | assert (List.of_seq result = l) 25 | 26 | let () = 27 | let x = (1, "1") in 28 | assert (Tuples.t2 ~x () = x) 29 | 30 | let () = 31 | let x = (1, "1", 1.0) in 32 | assert (Tuples.t3 ~x () = x) 33 | 34 | let () = 35 | let x = (1, "1", 1.0, true) in 36 | assert (Tuples.t4 ~x () = x) 37 | 38 | let () = 39 | let x = (1, "1", 1.0, true, 1) in 40 | assert (Tuples.t5 ~x () = x) 41 | 42 | let () = 43 | let x = [ (1, "1", 1.0, true, 1) ] in 44 | assert (Tuples.t5_list ~x () = x) 45 | 46 | let x = 1 47 | 48 | let y = 2 49 | 50 | let py_tup = (Py.Int.of_int x, Py.Int.of_int y) 51 | 52 | let () = 53 | let x', y' = Tuples.t2_pyobject ~x:py_tup () in 54 | let x', y' = (Py.Int.to_int x', Py.Int.to_int y') in 55 | assert ((x, y) = (x', y')) 56 | 57 | let () = 58 | let x', y' = Tuples.t2_pyobject2 ~x:py_tup () in 59 | let x', y' = (Py.Int.to_int x', Py.Int.to_int y') in 60 | assert ((x, y) = (x', y')) 61 | 62 | let () = 63 | let x', y' = 64 | match Tuples.t2_pyobject_list ~x:[ py_tup ] () with 65 | | [ a ] -> a 66 | | _ -> assert false 67 | in 68 | let x', y' = (Py.Int.to_int x', Py.Int.to_int y') in 69 | assert ((x, y) = (x', y')) 70 | 71 | let () = 72 | let x', y' = 73 | match Tuples.t2_pyobject2_list ~x:[ py_tup ] () with 74 | | [ a ] -> a 75 | | _ -> assert false 76 | in 77 | let x', y' = (Py.Int.to_int x', Py.Int.to_int y') in 78 | assert ((x, y) = (x', y')) 79 | 80 | let () = 81 | let points1 = [ (1, 2); (3, 4) ] in 82 | let points2 = [ (10, 20); (30, 40) ] in 83 | let actual = Tuples.add ~points1 ~points2 () in 84 | let expected = [ (11, 22); (33, 44) ] in 85 | assert (actual = expected) 86 | -------------------------------------------------------------------------------- /examples/tuples/test/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (deps 5 | ../run.exe 6 | (source_tree ..))) 7 | -------------------------------------------------------------------------------- /examples/tuples/test/run.t: -------------------------------------------------------------------------------- 1 | Run the executable. 2 | 3 | $ PYTHONPATH=$(pwd)/.. ../run.exe 4 | -------------------------------------------------------------------------------- /examples/tuples/tuples.py: -------------------------------------------------------------------------------- 1 | def pair(x, y): 2 | return (x, y) 3 | 4 | 5 | def first(x): 6 | return x[0] 7 | 8 | 9 | def identity(x): 10 | return x 11 | 12 | 13 | def make(x=(0, 0)): 14 | return x 15 | 16 | 17 | def add(points1, points2): 18 | return [(x1 + y1, x2 + y2) for (x1, x2), (y1, y2) in zip(points1, points2)] 19 | -------------------------------------------------------------------------------- /examples/tuples/val_specs.txt: -------------------------------------------------------------------------------- 1 | val pair : x:int -> y:string -> unit -> int * string 2 | val identity : x:int * int -> unit -> int * int 3 | val first : x:int * int -> unit -> int 4 | val make : ?x:int * int -> unit -> int * int 5 | val apple : x:int list -> unit -> int list 6 | [@@py_fun_name identity] 7 | 8 | val pie_list : x:(int * int) list -> unit -> (int * int) list 9 | [@@py_fun_name identity] 10 | 11 | val pie_array : x:(int * int) array -> unit -> (int * int) array 12 | [@@py_fun_name identity] 13 | 14 | val pie_seq : x:(int * int) Seq.t -> unit -> (int * int) Seq.t 15 | [@@py_fun_name identity] 16 | 17 | val t2 : x:int * string -> unit -> int * string 18 | [@@py_fun_name identity] 19 | 20 | val t3 : x:int * string * float -> unit -> int * string * float 21 | [@@py_fun_name identity] 22 | 23 | val t4 : x:int * string * float * bool -> unit -> int * string * float * bool 24 | [@@py_fun_name identity] 25 | 26 | val t5 : x:int * string * float * bool * int -> unit -> int * string * float * bool * int 27 | [@@py_fun_name identity] 28 | 29 | val t5_list : x:(int * string * float * bool * int) list -> unit -> (int * string * float * bool * int) list 30 | [@@py_fun_name identity] 31 | 32 | val t2_pyobject : x:Py.Object.t * Pytypes.pyobject -> unit -> Py.Object.t * Pytypes.pyobject 33 | [@@py_fun_name identity] 34 | 35 | val t2_pyobject2 : x:Pytypes.pyobject * Py.Object.t -> unit -> Pytypes.pyobject * Py.Object.t 36 | [@@py_fun_name identity] 37 | 38 | val t2_pyobject_list : x:(Py.Object.t * Pytypes.pyobject) list -> unit -> (Py.Object.t * Pytypes.pyobject) list 39 | [@@py_fun_name identity] 40 | 41 | val t2_pyobject2_list : x:(Pytypes.pyobject * Py.Object.t) list -> unit -> (Pytypes.pyobject * Py.Object.t) list 42 | [@@py_fun_name identity] 43 | 44 | val add : points1:(int * int) list -> points2:(int * int) list -> unit -> (int * int) list 45 | 46 | # NOT ALLOWED because only basic types can be in tuples. 47 | # val weird : x:int -> unit -> int * int list 48 | -------------------------------------------------------------------------------- /lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name lib) 3 | (libraries angstrom base re stdio unix) 4 | (instrumentation 5 | (backend bisect_ppx)) 6 | (preprocess 7 | (pps ppx_let ppx_sexp_conv ppx_string))) 8 | -------------------------------------------------------------------------------- /lib/mkdir.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | 3 | type file_perm = int [@@deriving of_sexp] 4 | 5 | module Mkdir : sig 6 | val mkdir : ?perm:file_perm -> string -> unit 7 | 8 | val mkdir_p : ?perm:file_perm -> string -> unit 9 | end = struct 10 | let atom x = Sexp.Atom x 11 | 12 | let list x = Sexp.List x 13 | 14 | let record l = 15 | list (List.map l ~f:(fun (name, value) -> list [ atom name; value ])) 16 | 17 | (* This wrapper improves the content of the Unix_error exception raised by the 18 | standard library (by including a sexp of the function arguments), and it 19 | optionally restarts syscalls on EINTR. *) 20 | let improve f make_arg_sexps = 21 | try f () 22 | with Unix.Unix_error (e, s, _) -> 23 | let buf = Buffer.create 100 in 24 | let fmt = Caml.Format.formatter_of_buffer buf in 25 | Caml.Format.pp_set_margin fmt 10000; 26 | Sexp.pp_hum fmt (record (make_arg_sexps ())); 27 | Caml.Format.pp_print_flush fmt (); 28 | let arg_str = Buffer.contents buf in 29 | raise (Unix.Unix_error (e, s, arg_str)) 30 | 31 | let dirname_r filename = ("dirname", atom filename) 32 | 33 | let file_perm_r perm = ("perm", atom (Printf.sprintf "0o%o" perm)) 34 | 35 | let[@inline always] improve_mkdir mkdir dirname perm = 36 | improve 37 | (fun () -> mkdir dirname perm) 38 | (fun () -> [ dirname_r dirname; file_perm_r perm ]) 39 | 40 | let mkdir = improve_mkdir Unix.mkdir 41 | 42 | let mkdir_idempotent dirname perm = 43 | match Unix.mkdir dirname perm with 44 | | () -> () 45 | (* [mkdir] on MacOSX returns [EISDIR] instead of [EEXIST] if the directory 46 | already exists. *) 47 | | exception Unix.Unix_error ((EEXIST | EISDIR), _, _) -> () 48 | 49 | let mkdir_idempotent = improve_mkdir mkdir_idempotent 50 | 51 | let rec mkdir_p dir perm = 52 | match mkdir_idempotent dir perm with 53 | | () -> () 54 | | exception (Unix.Unix_error (ENOENT, _, _) as exn) -> 55 | let parent = Caml.Filename.dirname dir in 56 | if String.( = ) parent dir then raise exn 57 | else ( 58 | mkdir_p parent perm; 59 | mkdir_idempotent dir perm) 60 | 61 | let mkdir ?(perm = 0o750) dir = mkdir dir perm 62 | 63 | let mkdir_p ?(perm = 0o750) dir = mkdir_p dir perm 64 | end 65 | 66 | include Mkdir 67 | 68 | (* Apapted from JaneStreet Core_unix. Original license follows. *) 69 | (* The MIT License 70 | 71 | Copyright (c) 2008--2022 Jane Street Group, LLC opensource@janestreet.com 72 | 73 | Permission is hereby granted, free of charge, to any person obtaining a copy 74 | of this software and associated documentation files (the "Software"), to deal 75 | in the Software without restriction, including without limitation the rights 76 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 77 | copies of the Software, and to permit persons to whom the Software is 78 | furnished to do so, subject to the following conditions: 79 | 80 | The above copyright notice and this permission notice shall be included in 81 | all copies or substantial portions of the Software. 82 | 83 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 84 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 85 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 86 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 87 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 88 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 89 | SOFTWARE. *) 90 | -------------------------------------------------------------------------------- /lib/shared.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | 3 | (** Note, this one isn't included in the "all" functions below. *) 4 | let gen_import_module_impl ?python_source py_module = 5 | match python_source with 6 | | None -> 7 | [%string 8 | "let py_module = lazy (Py.Import.import_module \"%{py_module}\")\n\ 9 | let import_module () = Lazy.force py_module"] 10 | | Some file_name -> 11 | let source = Utils.read_python_source file_name in 12 | [%string 13 | "let py_module = lazy (\n\ 14 | \ let source = \ 15 | {pyml_bindgen_string_literal|%{source}|pyml_bindgen_string_literal} in\n\ 16 | \ let filename = \ 17 | {pyml_bindgen_string_literal|%{file_name}|pyml_bindgen_string_literal} \ 18 | in\n\ 19 | \ let bytecode = Py.compile ~filename ~source `Exec in\n\ 20 | \ Py.Import.exec_code_module \ 21 | {pyml_bindgen_string_literal|%{py_module}|pyml_bindgen_string_literal} \ 22 | bytecode)\n\ 23 | let import_module () = Lazy.force py_module"] 24 | 25 | let gen_type_sig () = "type t" 26 | 27 | let gen_type_impl () = "type t = Pytypes.pyobject" 28 | 29 | let gen_of_pyobject_sig = function 30 | | `No_check -> "val of_pyobject : Pytypes.pyobject -> t" 31 | | `Option -> "val of_pyobject : Pytypes.pyobject -> t option" 32 | | `Or_error -> "val of_pyobject : Pytypes.pyobject -> t Or_error.t" 33 | 34 | (** Only generates stuff for simple python types, or for custom classes. If you 35 | have a list you will need to write your own. *) 36 | let gen_of_pyobject_impl of_pyo_return_type of_pyo_otype = 37 | match (of_pyo_return_type, of_pyo_otype) with 38 | | `Option, `Custom py_class -> 39 | [%string 40 | {| 41 | let is_instance pyo = 42 | let py_class = Py.Module.get (import_module ()) "%{py_class}" in 43 | Py.Object.is_instance pyo py_class 44 | 45 | let of_pyobject pyo = if is_instance pyo then Some pyo else None 46 | |}] 47 | | `Or_error, `Custom py_class -> 48 | [%string 49 | {| 50 | let is_instance pyo = 51 | let py_class = Py.Module.get (import_module ()) "%{py_class}" in 52 | Py.Object.is_instance pyo py_class 53 | 54 | let of_pyobject pyo = 55 | if is_instance pyo then Or_error.return pyo 56 | else Or_error.error_string "Expected %{py_class}" 57 | |}] 58 | | `Option, `Int -> 59 | [%string 60 | {| let of_pyobject pyo = if Py.Int.check pyo then Some pyo else None |}] 61 | | `Or_error, `Int -> 62 | [%string 63 | {| 64 | let of_pyobject pyo = 65 | if Py.Int.check pyo then Or_error.return 66 | else Or_error.error_string "Expected Int" 67 | |}] 68 | | `Option, `Float -> 69 | [%string 70 | {| let of_pyobject pyo = if Py.Float.check pyo then Some pyo else None |}] 71 | | `Or_error, `Float -> 72 | [%string 73 | {| 74 | let of_pyobject pyo = 75 | if Py.Float.check pyo then Or_error.return pyo 76 | else Or_error.error_string "Expected Float" 77 | |}] 78 | | `Option, `String -> 79 | [%string 80 | {| let of_pyobject pyo = if Py.String.check pyo then Some pyo else None |}] 81 | | `Or_error, `String -> 82 | [%string 83 | {| 84 | let of_pyobject pyo = 85 | if Py.String.check pyo then Or_error.return pyo 86 | else Or_error.error_string "Expected String" 87 | |}] 88 | | `Option, `Bool -> 89 | [%string 90 | {| let of_pyobject pyo = if Py.Bool.check pyo then Some pyo else None |}] 91 | | `Or_error, `Bool -> 92 | [%string 93 | {| 94 | let of_pyobject pyo = 95 | if Py.Bool.check pyo then Or_error.return pyo 96 | else Or_error.error_string "Expected Bool" 97 | |}] 98 | | `No_check, `Custom _ 99 | | `No_check, `Int 100 | | `No_check, `Float 101 | | `No_check, `String 102 | | `No_check, `Bool -> 103 | "let of_pyobject pyo = pyo" 104 | 105 | let gen_to_pyobject_sig () = "val to_pyobject : t -> Pytypes.pyobject" 106 | 107 | let gen_to_pyobject_impl () = "let to_pyobject x = x" 108 | 109 | let gen_all_signatures of_pyo_return_type = 110 | [ 111 | gen_type_sig (); 112 | gen_of_pyobject_sig of_pyo_return_type; 113 | gen_to_pyobject_sig (); 114 | ] 115 | 116 | let gen_all_functions of_pyo_return_type of_pyo_otype = 117 | [ 118 | gen_type_impl (); 119 | gen_of_pyobject_impl of_pyo_return_type of_pyo_otype; 120 | gen_to_pyobject_impl (); 121 | ] 122 | -------------------------------------------------------------------------------- /lib/specs_file.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | 3 | (* TODO ideally we would integrate this parsing step with our other parsers and 4 | then have a single "specs file" parser. *) 5 | 6 | type spec = { attrs : string option; val_spec : string } [@@deriving sexp] 7 | 8 | let is_comment_line s = String.is_prefix ~prefix:"#" s 9 | 10 | let is_val_start s = String.is_prefix ~prefix:"val" s 11 | 12 | let all_whitespace = Re.compile @@ Re.Perl.re "^\\s*$" 13 | 14 | let is_all_whitespace s = Re.execp all_whitespace s 15 | 16 | let comment_marker = Re.compile @@ Re.Perl.re "^#\\s*" 17 | 18 | let cat s1 s2 = s1 ^ " " ^ s2 19 | 20 | (* TODO should we include 0-9 here? *) 21 | (* TODO We are being more restrictive than normal in that each attr must be on 22 | its own line, and only one per line. *) 23 | let attribute_line = 24 | Re.compile @@ Re.Perl.re "^\\s*\\[@@[a-zA-Z_]+\\s+[a-zA-Z_ ]+\\]\\s*$" 25 | 26 | let attributes_not_at_start = 27 | Re.compile @@ Re.Perl.re "^\\S+.*\\[@@[a-zA-Z_]+\\s+[a-zA-Z_ ]+\\]" 28 | 29 | let has_attributes_not_at_start line = Re.execp attributes_not_at_start line 30 | 31 | let is_attribute_line line = Re.execp attribute_line line 32 | 33 | (* assert false cases should be impossible unless I made a mistake. failwith 34 | cases can happen with bad user input in the file. *) 35 | let read fname = 36 | let open Stdio in 37 | let lines = 38 | List.filter ~f:(fun l -> 39 | (not (is_all_whitespace l)) && not (is_comment_line l)) 40 | @@ In_channel.read_lines fname 41 | in 42 | let current_attr, current_val_spec, specs = 43 | List.fold lines ~init:(None, None, []) 44 | ~f:(fun (attrs, val_spec, all) line -> 45 | let line = String.strip line in 46 | if has_attributes_not_at_start line then 47 | failwith "attributes must start a line"; 48 | match (is_attribute_line line, is_val_start line, attrs, val_spec) with 49 | | true, true, None, None 50 | | true, true, None, Some _ 51 | | true, true, Some _, None 52 | | true, true, Some _, Some _ -> 53 | assert false 54 | (* In an attribute line *) 55 | | true, false, None, None -> 56 | failwith "We have attributes but no val spec for them to go with." 57 | | true, false, Some _, None -> assert false 58 | | true, false, None, Some current_val_spec -> 59 | let new_attrs = line in 60 | (Some new_attrs, Some current_val_spec, all) 61 | | true, false, Some current_attrs, Some current_val_spec -> 62 | let new_attrs = line in 63 | (Some (cat current_attrs new_attrs), Some current_val_spec, all) 64 | (* Starting a new val spec *) 65 | | false, true, None, None -> 66 | let new_val_spec = line in 67 | (None, Some new_val_spec, all) 68 | | false, true, None, Some current_val_spec -> 69 | (* Track the old val spec. *) 70 | let all = { attrs = None; val_spec = current_val_spec } :: all in 71 | (* Set up the new one. *) 72 | let new_val_spec = line in 73 | (None, Some new_val_spec, all) 74 | | false, true, Some _, None -> assert false 75 | | false, true, Some current_attrs, Some current_val_spec -> 76 | (* Track the old val spec. *) 77 | let all = 78 | { attrs = Some current_attrs; val_spec = current_val_spec } :: all 79 | in 80 | (* Set up the new one. *) 81 | let new_val_spec = line in 82 | (None, Some new_val_spec, all) 83 | (* In the middle of a val spec *) 84 | | false, false, None, None -> 85 | failwith "In the middle of a val spec, but have none to work on." 86 | | false, false, None, Some current_val_spec -> 87 | let new_val_spec = line in 88 | (None, Some (cat current_val_spec new_val_spec), all) 89 | | false, false, Some _, None -> assert false 90 | | false, false, Some _, Some _ -> 91 | failwith "Found unused attrs but in the middle of a val spec.") 92 | in 93 | (* Finish off last spec if it is there. *) 94 | let specs = 95 | match (current_attr, current_val_spec) with 96 | | None, None -> specs 97 | | None, Some val_spec -> { attrs = None; val_spec } :: specs 98 | | Some _, None -> 99 | prerr_endline 100 | "WARNING: currently in a attributes line but hit EOF without getting \ 101 | a val_spec to go with it."; 102 | specs 103 | | Some attrs, Some val_spec -> { attrs = Some attrs; val_spec } :: specs 104 | in 105 | List.rev specs 106 | -------------------------------------------------------------------------------- /lib/utils.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | 3 | let is_space = function ' ' -> true | _ -> false 4 | 5 | let is_capital_letter = function 'A' .. 'Z' -> true | _ -> false 6 | 7 | let is_lowercase_letter = function 'a' .. 'z' -> true | _ -> false 8 | 9 | let is_ok_for_name = function 10 | | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '_' -> true 11 | | _ -> false 12 | 13 | let read_python_source file_name = Stdio.In_channel.read_all file_name 14 | 15 | module Angstrom_helpers = struct 16 | open Angstrom 17 | 18 | let is_whitepace = function ' ' | '\t' -> true | _ -> false 19 | 20 | let is_whitespace_or_eol = function 21 | | ' ' | '\t' | '\r' | '\n' -> true 22 | | _ -> false 23 | 24 | let eoi = 25 | let open Angstrom.Let_syntax in 26 | end_of_input 27 | <|> let%bind bad_thing = take_till is_whitespace_or_eol in 28 | fail bad_thing 29 | "parser failed before all input was consumed at token" 30 | end 31 | 32 | let spaces = Re.compile @@ Re.Perl.re "[ \n]+" 33 | 34 | let squash_spaces s = Re.replace_string spaces s ~by:" " 35 | 36 | let clean s = String.strip @@ squash_spaces s 37 | 38 | let todo_type = "type 'a todo = unit -> 'a" 39 | 40 | let not_implemented_type = "type 'a not_implemented = unit -> 'a" 41 | 42 | let or_error_re = Re.compile @@ Re.Perl.re "Or_error\\.t" 43 | 44 | let todo_re = Re.compile @@ Re.Perl.re "'a todo" 45 | 46 | let not_implemented_re = Re.compile @@ Re.Perl.re "'a not_implemented" 47 | 48 | (* TODO move all these check needs functions up into the one in pyml_bindgen 49 | main. *) 50 | 51 | (* This would give false positives if the Or_error is in something other than 52 | the return type. Although, other functions should prevent valid val_specs 53 | from having or error anywhere else. *) 54 | let check_needs_base s = Re.execp or_error_re s 55 | 56 | let check_needs_todo s = Re.execp todo_re s 57 | 58 | let check_needs_not_implemented s = Re.execp not_implemented_re s 59 | 60 | let check_signatures_file fname = 61 | let sig_dat = Stdio.In_channel.read_all fname in 62 | let needs_base = check_needs_base sig_dat in 63 | let needs_todo = check_needs_todo sig_dat in 64 | let needs_not_implemented = check_needs_not_implemented sig_dat in 65 | (needs_base, needs_todo, needs_not_implemented) 66 | 67 | let print_dbl_endline s = Stdio.print_endline (s ^ "\n") 68 | 69 | let abort ?(exit_code = 1) msg = 70 | Stdio.prerr_endline ("ERROR: " ^ msg); 71 | Caml.exit exit_code 72 | 73 | let find_first re s ~sub = 74 | match Re.exec_opt re s with 75 | | None -> Or_error.error_string "regex did not match" 76 | | Some group -> ( 77 | match Re.Group.get_opt group sub with 78 | | None -> Or_error.error_string "group did not match" 79 | | Some thing -> Or_error.return thing) 80 | 81 | let py_fun_name_attribute = 82 | (* TODO do we need 0-9 in there? *) 83 | Re.compile @@ Re.Perl.re "\\[@@py_fun_name\\s+([a-zA-Z_]+)\\]" 84 | 85 | let get_py_fun_name s = find_first py_fun_name_attribute s ~sub:1 86 | 87 | let py_arg_name_attribute = 88 | Re.Perl.compile_pat 89 | "\\[@@py_arg_name\\s+([a-zA-Z0-9_]+)\\s+([a-zA-Z0-9_]+)\\]" 90 | 91 | (* Given the attributes on a val_spec, pull out any mappings from ml_name to 92 | py_name. *) 93 | let get_arg_name_map attrs = 94 | match attrs with 95 | | None -> Map.empty (module String) 96 | | Some attrs -> 97 | let py_arg_names = 98 | Re.all py_arg_name_attribute attrs 99 | |> List.fold 100 | ~init:(Map.empty (module String)) 101 | ~f:(fun m g -> 102 | (* For now, blow up if a key is duplicated. TODO *) 103 | Map.add_exn m ~key:(Re.Group.get g 1) ~data:(Re.Group.get g 2)) 104 | in 105 | py_arg_names 106 | 107 | let find_with_default m ~key ~default = Option.value ~default @@ Map.find m key 108 | 109 | let downcase_first_letter s = 110 | let first = String.prefix s 1 in 111 | let rest = String.drop_prefix s 1 in 112 | String.lowercase first ^ rest 113 | -------------------------------------------------------------------------------- /lib/version.ml: -------------------------------------------------------------------------------- 1 | let version = "0.4.0-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /pyml_bindgen-dev.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | version: "0.4.1" 4 | synopsis: "Development package for pyml_bindgen" 5 | maintainer: ["Ryan M. Moore"] 6 | authors: ["Ryan M. Moore"] 7 | homepage: "https://github.com/mooreryan/ocaml_python_bindgen" 8 | doc: "https://mooreryan.github.io/ocaml_python_bindgen/" 9 | bug-reports: "https://github.com/mooreryan/ocaml_python_bindgen/issues" 10 | depends: [ 11 | "dune" {>= "3.0"} 12 | "ocaml" {>= "4.14"} 13 | "bisect_ppx" 14 | "core" {>= "v0.15"} 15 | "core_bench" {>= "v0.15"} 16 | "core_unix" {>= "v0.15"} 17 | "ocamlformat" {>= "0.23" & < "0.24"} 18 | "ocaml-lsp-server" {>= "1.13"} 19 | "pyml" 20 | "odoc" {with-doc} 21 | ] 22 | build: [ 23 | ["dune" "subst"] {dev} 24 | [ 25 | "dune" 26 | "build" 27 | "-p" 28 | name 29 | "-j" 30 | jobs 31 | "@install" 32 | "@runtest" {with-test} 33 | "@doc" {with-doc} 34 | ] 35 | ] 36 | dev-repo: "git+https://github.com/mooreryan/ocaml_python_bindgen.git" 37 | -------------------------------------------------------------------------------- /pyml_bindgen.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | version: "0.4.1" 4 | synopsis: "Generate pyml bindings from OCaml value specifications" 5 | maintainer: ["Ryan M. Moore"] 6 | authors: ["Ryan M. Moore"] 7 | homepage: "https://github.com/mooreryan/ocaml_python_bindgen" 8 | doc: "https://mooreryan.github.io/ocaml_python_bindgen/" 9 | bug-reports: "https://github.com/mooreryan/ocaml_python_bindgen/issues" 10 | depends: [ 11 | "dune" {>= "3.0"} 12 | "angstrom" {>= "0.15.0"} 13 | "base" {>= "v0.12"} 14 | "cmdliner" {>= "1.1.0"} 15 | "ppx_let" {>= "v0.12"} 16 | "ppx_sexp_conv" {>= "v0.12"} 17 | "ppx_string" {>= "v0.12"} 18 | "re" {>= "1.10.0"} 19 | "stdio" {>= "v0.12"} 20 | "ocaml" {>= "4.08.0"} 21 | "conf-python-3-dev" {>= "1" & with-test} 22 | "base_quickcheck" {>= "v0.12" & with-test} 23 | "ocamlformat" {>= "0.23" & < "0.24" & with-test} 24 | "ppx_assert" {>= "v0.12" & with-test} 25 | "ppx_inline_test" {>= "v0.12" & with-test} 26 | "ppx_expect" {>= "v0.12" & with-test} 27 | "pyml" {with-test} 28 | "shexp" {>= "v0.14" & with-test} 29 | "odoc" {with-doc} 30 | ] 31 | build: [ 32 | ["dune" "subst"] {dev} 33 | [ 34 | "dune" 35 | "build" 36 | "-p" 37 | name 38 | "-j" 39 | jobs 40 | "@install" 41 | "@runtest" {with-test} 42 | "@doc" {with-doc} 43 | ] 44 | ] 45 | dev-repo: "git+https://github.com/mooreryan/ocaml_python_bindgen.git" 46 | license: ["MIT" "Apache-2.0"] 47 | -------------------------------------------------------------------------------- /pyml_bindgen.opam.template: -------------------------------------------------------------------------------- 1 | license: ["MIT" "Apache-2.0"] 2 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | **The cram tests in the `of_pyo_*.t` are all pretty similar, but there are slight differences between them that may trip you up if you go to update them but you're not paying close enough attention.** 2 | -------------------------------------------------------------------------------- /test/basic_class_binding.t/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names run_no_check run_option run_or_error) 3 | (libraries base pyml stdio) 4 | (preprocess (pps ppx_sexp_conv))) 5 | -------------------------------------------------------------------------------- /test/basic_class_binding.t/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | -------------------------------------------------------------------------------- /test/basic_class_binding.t/run_no_check.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Lib 3 | open Stdio 4 | 5 | let () = Py.initialize () 6 | 7 | let silly = Silly.__init__ ~x:1 ~y:2 () 8 | 9 | let () = print_endline ("x: " ^ Int.to_string (Silly.x silly)) 10 | let () = print_endline ("y: " ^ Int.to_string (Silly.y silly)) 11 | let () = print_endline ("foo: " ^ Int.to_string (Silly.foo silly ~a:10 ~b:20 ())) 12 | let () = print_endline ("bar: " ^ Int.to_string (Silly.bar ~a:10 ~b:20 ())) 13 | 14 | let () = Silly.do_nothing silly () 15 | let () = Silly.do_nothing2 () 16 | 17 | let () = 18 | print_endline 19 | @@ Sexp.to_string_hum ~indent:1 20 | @@ [%sexp_of: string list] 21 | @@ Silly.return_list silly ~l:[ "apple"; "pie" ] () 22 | let () = 23 | print_endline 24 | @@ Sexp.to_string_hum ~indent:1 25 | @@ [%sexp_of: string option list] 26 | @@ Silly.return_opt_list silly ~l:[ Some "apple"; None; Some "pie" ] () 27 | 28 | let () = 29 | print_endline 30 | @@ Sexp.to_string_hum ~indent:1 31 | @@ [%sexp_of: string array] 32 | @@ Silly.return_array silly ~a:[| "apple"; "pie" |] () 33 | let () = 34 | print_endline 35 | @@ Sexp.to_string_hum ~indent:1 36 | @@ [%sexp_of: string option array] 37 | @@ Silly.return_opt_array silly ~a:[| Some "apple"; None; Some "pie" |] () 38 | -------------------------------------------------------------------------------- /test/basic_class_binding.t/run_option.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Lib 3 | open Stdio 4 | 5 | let () = Py.initialize () 6 | 7 | let silly = Option.value_exn (Silly.__init__ ~x:1 ~y:2 ()) 8 | 9 | let () = print_endline ("x: " ^ Int.to_string (Silly.x silly)) 10 | let () = print_endline ("y: " ^ Int.to_string (Silly.y silly)) 11 | let () = print_endline ("foo: " ^ Int.to_string (Silly.foo silly ~a:10 ~b:20 ())) 12 | let () = print_endline ("bar: " ^ Int.to_string (Silly.bar ~a:10 ~b:20 ())) 13 | 14 | let () = Silly.do_nothing silly () 15 | let () = Silly.do_nothing2 () 16 | 17 | let () = 18 | print_endline 19 | @@ Sexp.to_string_hum ~indent:1 20 | @@ [%sexp_of: string list] 21 | @@ Silly.return_list silly ~l:[ "apple"; "pie" ] () 22 | let () = 23 | print_endline 24 | @@ Sexp.to_string_hum ~indent:1 25 | @@ [%sexp_of: string option list] 26 | @@ Silly.return_opt_list silly ~l:[ Some "apple"; None; Some "pie" ] () 27 | 28 | let () = 29 | print_endline 30 | @@ Sexp.to_string_hum ~indent:1 31 | @@ [%sexp_of: string array] 32 | @@ Silly.return_array silly ~a:[| "apple"; "pie" |] () 33 | let () = 34 | print_endline 35 | @@ Sexp.to_string_hum ~indent:1 36 | @@ [%sexp_of: string option array] 37 | @@ Silly.return_opt_array silly ~a:[| Some "apple"; None; Some "pie" |] () 38 | -------------------------------------------------------------------------------- /test/basic_class_binding.t/run_or_error.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Lib 3 | open Stdio 4 | 5 | let () = Py.initialize () 6 | 7 | let silly = Or_error.ok_exn @@ Silly.__init__ ~x:1 ~y:2 () 8 | 9 | let () = print_endline ("x: " ^ Int.to_string (Silly.x silly)) 10 | let () = print_endline ("y: " ^ Int.to_string (Silly.y silly)) 11 | let () = print_endline ("foo: " ^ Int.to_string (Silly.foo silly ~a:10 ~b:20 ())) 12 | let () = print_endline ("bar: " ^ Int.to_string (Silly.bar ~a:10 ~b:20 ())) 13 | 14 | let () = Silly.do_nothing silly () 15 | let () = Silly.do_nothing2 () 16 | 17 | let () = 18 | print_endline 19 | @@ Sexp.to_string_hum ~indent:1 20 | @@ [%sexp_of: string list] 21 | @@ Silly.return_list silly ~l:[ "apple"; "pie" ] () 22 | let () = 23 | print_endline 24 | @@ Sexp.to_string_hum ~indent:1 25 | @@ [%sexp_of: string option list] 26 | @@ Silly.return_opt_list silly ~l:[ Some "apple"; None; Some "pie" ] () 27 | 28 | let () = 29 | print_endline 30 | @@ Sexp.to_string_hum ~indent:1 31 | @@ [%sexp_of: string array] 32 | @@ Silly.return_array silly ~a:[| "apple"; "pie" |] () 33 | let () = 34 | print_endline 35 | @@ Sexp.to_string_hum ~indent:1 36 | @@ [%sexp_of: string option array] 37 | @@ Silly.return_opt_array silly ~a:[| Some "apple"; None; Some "pie" |] () 38 | -------------------------------------------------------------------------------- /test/basic_class_binding.t/sigs_no_check.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:int -> y:int -> unit -> t 2 | 3 | # Attributes 4 | val x : t -> int 5 | val y : t -> int 6 | 7 | # Instance method 8 | val foo : t -> a:int -> b:int -> unit -> int 9 | val do_nothing : t -> unit -> unit 10 | val return_list : t -> l:string list -> unit -> string list 11 | val return_opt_list : t -> l:string option list -> unit -> string option list 12 | val return_array : t -> a:string array -> unit -> string array 13 | val return_opt_array : t -> a:string option array -> unit -> string option array 14 | 15 | # Class method 16 | val bar : a:int -> b:int -> unit -> int 17 | val do_nothing2 : unit -> unit 18 | 19 | # Placeholders 20 | val hello : 'a todo 21 | val world : 'a not_implemented 22 | -------------------------------------------------------------------------------- /test/basic_class_binding.t/sigs_option.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:int -> y:int -> unit -> t option 2 | 3 | # Attributes 4 | val x : t -> int 5 | val y : t -> int 6 | 7 | # Instance method 8 | val foo : t -> a:int -> b:int -> unit -> int 9 | val do_nothing : t -> unit -> unit 10 | val return_list : t -> l:string list -> unit -> string list 11 | val return_opt_list : t -> l:string option list -> unit -> string option list 12 | val return_array : t -> a:string array -> unit -> string array 13 | val return_opt_array : t -> a:string option array -> unit -> string option array 14 | 15 | # Class method 16 | val bar : a:int -> b:int -> unit -> int 17 | val do_nothing2 : unit -> unit 18 | 19 | # Placeholders 20 | val hello : 'a todo 21 | val world : 'a not_implemented 22 | -------------------------------------------------------------------------------- /test/basic_class_binding.t/sigs_or_error.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:int -> y:int -> unit -> t Or_error.t 2 | 3 | # Attributes 4 | val x : t -> int 5 | val y : t -> int 6 | 7 | # Instance method 8 | val foo : t -> a:int -> b:int -> unit -> int 9 | val do_nothing : t -> unit -> unit 10 | val return_list : t -> l:string list -> unit -> string list 11 | val return_opt_list : t -> l:string option list -> unit -> string option list 12 | val return_array : t -> a:string array -> unit -> string array 13 | val return_opt_array : t -> a:string option array -> unit -> string option array 14 | 15 | # Class method 16 | val bar : a:int -> b:int -> unit -> int 17 | val do_nothing2 : unit -> unit 18 | 19 | # Placeholders 20 | val hello : 'a todo 21 | val world : 'a not_implemented 22 | -------------------------------------------------------------------------------- /test/basic_class_binding.t/silly.py: -------------------------------------------------------------------------------- 1 | class Silly: 2 | def __init__(self, x, y): 3 | self.x = x 4 | self.y = y 5 | 6 | def foo(self, a, b): 7 | return a + b + self.x + self.y 8 | 9 | def do_nothing(self): 10 | return None 11 | 12 | def return_list(self, l): 13 | return l 14 | 15 | def return_opt_list(self, l): 16 | return l 17 | 18 | def return_array(self, a): 19 | return a 20 | 21 | def return_opt_array(self, a): 22 | return a 23 | 24 | @staticmethod 25 | def bar(a, b): 26 | return a + b 27 | 28 | @staticmethod 29 | def do_nothing2(): 30 | return None 31 | -------------------------------------------------------------------------------- /test/binding_tuples.t/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name run) 3 | (libraries pyml)) 4 | -------------------------------------------------------------------------------- /test/binding_tuples.t/lib_ml.txt: -------------------------------------------------------------------------------- 1 | module Tuple_int_string : sig 2 | type t 3 | 4 | val make : int -> string -> t 5 | 6 | val to_pyobject : t -> Pytypes.pyobject 7 | 8 | val of_pyobject : Pytypes.pyobject -> t 9 | 10 | val print_endline : t -> unit 11 | end = struct 12 | type t = int * string 13 | 14 | let make i s = (i, s) 15 | 16 | let to_pyobject (i, s) = 17 | Py.Tuple.of_tuple2 (Py.Int.of_int i, Py.String.of_string s) 18 | 19 | let of_pyobject pyo = 20 | let i, s = Py.Tuple.to_tuple2 pyo in 21 | (Py.Int.to_int i, Py.String.to_string s) 22 | 23 | let print_endline (i, s) = 24 | print_endline @@ (string_of_int i) ^ " " ^ s 25 | end 26 | -------------------------------------------------------------------------------- /test/binding_tuples.t/run.ml: -------------------------------------------------------------------------------- 1 | open Lib 2 | 3 | let () = Py.initialize () 4 | 5 | let () = 6 | Tuple_int_string.print_endline 7 | @@ Silly.foo ~x:(Tuple_int_string.make 1 "a") () 8 | -------------------------------------------------------------------------------- /test/binding_tuples.t/run.t: -------------------------------------------------------------------------------- 1 | These are a little brittle...they will break if ocamlformat changes 2 | the way it formats, but it is so much easier to read the generated 3 | code if it is properly formatted. 4 | 5 | Setup env 6 | 7 | $ export SANITIZE_LOGS=$PWD/../helpers/sanitize_logs 8 | 9 | Binding tuples 10 | 11 | $ if [ -f tmp ]; then rm tmp; fi 12 | $ pyml_bindgen sigs.txt silly Silly --caml-module=Silly --of-pyo-ret-type=no_check > tmp 13 | 14 | Cat the files and run. 15 | 16 | $ cat lib_ml.txt > lib.ml 17 | $ printf "\n" >> lib.ml 18 | $ cat tmp >> lib.ml 19 | $ ocamlformat --enable-outside-detected-project lib.ml 20 | module Tuple_int_string : sig 21 | type t 22 | 23 | val make : int -> string -> t 24 | val to_pyobject : t -> Pytypes.pyobject 25 | val of_pyobject : Pytypes.pyobject -> t 26 | val print_endline : t -> unit 27 | end = struct 28 | type t = int * string 29 | 30 | let make i s = (i, s) 31 | 32 | let to_pyobject (i, s) = 33 | Py.Tuple.of_tuple2 (Py.Int.of_int i, Py.String.of_string s) 34 | 35 | let of_pyobject pyo = 36 | let i, s = Py.Tuple.to_tuple2 pyo in 37 | (Py.Int.to_int i, Py.String.to_string s) 38 | 39 | let print_endline (i, s) = print_endline @@ string_of_int i ^ " " ^ s 40 | end 41 | 42 | module Silly : sig 43 | type t 44 | 45 | val of_pyobject : Pytypes.pyobject -> t 46 | val to_pyobject : t -> Pytypes.pyobject 47 | val foo : x:Tuple_int_string.t -> unit -> Tuple_int_string.t 48 | end = struct 49 | let filter_opt l = List.filter_map Fun.id l 50 | let py_module = lazy (Py.Import.import_module "silly") 51 | let import_module () = Lazy.force py_module 52 | 53 | type t = Pytypes.pyobject 54 | 55 | let of_pyobject pyo = pyo 56 | let to_pyobject x = x 57 | 58 | let foo ~x () = 59 | let class_ = Py.Module.get (import_module ()) "Silly" in 60 | let callable = Py.Object.find_attr_string class_ "foo" in 61 | let kwargs = filter_opt [ Some ("x", Tuple_int_string.to_pyobject x) ] in 62 | Tuple_int_string.of_pyobject 63 | @@ Py.Callable.to_function_with_keywords callable [||] kwargs 64 | end 65 | $ dune exec ./run.exe 2> /dev/null 66 | 10 a!! 67 | 68 | -------------------------------------------------------------------------------- /test/binding_tuples.t/sigs.txt: -------------------------------------------------------------------------------- 1 | val foo : x:Tuple_int_string.t -> unit -> Tuple_int_string.t 2 | -------------------------------------------------------------------------------- /test/binding_tuples.t/silly.py: -------------------------------------------------------------------------------- 1 | class Silly: 2 | @staticmethod 3 | def foo(x): 4 | return (x[0] * 10, x[1] + "!!") 5 | -------------------------------------------------------------------------------- /test/combine_rec_modules.t/abc.ml: -------------------------------------------------------------------------------- 1 | module Apple_pie2 : sig 2 | type t 3 | end = struct 4 | type t 5 | end 6 | 7 | module Bike__thing__242__ : sig 8 | type t 9 | end = struct 10 | type t 11 | end 12 | 13 | module Cake : sig 14 | type t 15 | end = struct 16 | type t 17 | end 18 | -------------------------------------------------------------------------------- /test/combine_rec_modules.t/d.ml: -------------------------------------------------------------------------------- 1 | module Donut : sig 2 | type t 3 | end = struct 4 | type t 5 | end 6 | -------------------------------------------------------------------------------- /test/combine_rec_modules.t/e.ml: -------------------------------------------------------------------------------- 1 | module Extra : sig 2 | type t 3 | end = struct 4 | type t 5 | end 6 | -------------------------------------------------------------------------------- /test/combine_rec_modules.t/no_modules.ml: -------------------------------------------------------------------------------- 1 | let x = 1 2 | -------------------------------------------------------------------------------- /test/combine_rec_modules.t/no_sig.ml: -------------------------------------------------------------------------------- 1 | module A = struct 2 | type t 3 | end 4 | 5 | module B = struct 6 | type t 7 | end 8 | -------------------------------------------------------------------------------- /test/combine_rec_modules.t/one_line.ml: -------------------------------------------------------------------------------- 1 | module A_a__3 : sig type t end = struct type t end 2 | 3 | module Bike : sig type t end = struct type t end 4 | 5 | module Cake_icing : sig type t end = struct type t end 6 | -------------------------------------------------------------------------------- /test/combine_rec_modules.t/run.t: -------------------------------------------------------------------------------- 1 | If you only see one module, exit with an error. It doesn't make sense to run it 2 | if there is only one module. 3 | 4 | $ combine_rec_modules d.ml 5 | module rec Donut : sig 6 | type t 7 | end = struct 8 | type t 9 | end 10 | ERROR -- I only saw one module in the input files 11 | [1] 12 | 13 | If you don't see any modules, exit with an error. 14 | 15 | $ combine_rec_modules no_modules.ml 16 | let x = 1 17 | ERROR -- I didn't see any modules in the input files 18 | [1] 19 | 20 | Basic usage 21 | 22 | $ combine_rec_modules abc.ml 23 | module rec Apple_pie2 : sig 24 | type t 25 | end = struct 26 | type t 27 | end 28 | 29 | and Bike__thing__242__ : sig 30 | type t 31 | end = struct 32 | type t 33 | end 34 | 35 | and Cake : sig 36 | type t 37 | end = struct 38 | type t 39 | end 40 | $ combine_rec_modules d.ml e.ml 41 | module rec Donut : sig 42 | type t 43 | end = struct 44 | type t 45 | end 46 | and Extra : sig 47 | type t 48 | end = struct 49 | type t 50 | end 51 | $ combine_rec_modules abc.ml d.ml e.ml 52 | module rec Apple_pie2 : sig 53 | type t 54 | end = struct 55 | type t 56 | end 57 | 58 | and Bike__thing__242__ : sig 59 | type t 60 | end = struct 61 | type t 62 | end 63 | 64 | and Cake : sig 65 | type t 66 | end = struct 67 | type t 68 | end 69 | and Donut : sig 70 | type t 71 | end = struct 72 | type t 73 | end 74 | and Extra : sig 75 | type t 76 | end = struct 77 | type t 78 | end 79 | 80 | It works okay when there is more than just the module on the line. 81 | 82 | $ combine_rec_modules one_line.ml 83 | module rec A_a__3 : sig type t end = struct type t end 84 | 85 | and Bike : sig type t end = struct type t end 86 | 87 | and Cake_icing : sig type t end = struct type t end 88 | 89 | It does NOT check if you have a signature. 90 | 91 | $ combine_rec_modules no_sig.ml 92 | module rec A = struct 93 | type t 94 | end 95 | 96 | and B = struct 97 | type t 98 | end 99 | 100 | Stdin works by passing /dev/stdin 101 | 102 | $ cat d.ml e.ml | combine_rec_modules /dev/stdin 103 | module rec Donut : sig 104 | type t 105 | end = struct 106 | type t 107 | end 108 | and Extra : sig 109 | type t 110 | end = struct 111 | type t 112 | end 113 | -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name test_lib) 3 | (libraries base base_quickcheck lib shexp.process stdio) 4 | (inline_tests) 5 | (preprocess 6 | (pps 7 | ppx_assert 8 | ppx_let 9 | ppx_sexp_conv 10 | ppx_string 11 | ppx_inline_test 12 | ppx_expect))) 13 | 14 | (cram 15 | (applies_to :whole_subtree) 16 | (deps 17 | %{bin:combine_rec_modules} 18 | %{bin:gen_multi} 19 | %{bin:pyml_bindgen} 20 | ./helpers/sanitize_logs)) 21 | -------------------------------------------------------------------------------- /test/embed_source.t/.ocamlformat: -------------------------------------------------------------------------------- 1 | profile = default 2 | version = 0.23.0 3 | parse-docstrings = true 4 | wrap-comments = true 5 | -------------------------------------------------------------------------------- /test/embed_source.t/bad_thing.py: -------------------------------------------------------------------------------- 1 | class Thing: 2 | """A thing is pretty basic. It does have a color though!""" 3 | 4 | def __init__(self, color): 5 | """This will break :) {pyml_bindgen_string_literal| any |pyml_bindgen_string_literal}.""" 6 | self.color = color 7 | 8 | def __str__(self): 9 | return(f'Thing -- color: {self.color}') 10 | -------------------------------------------------------------------------------- /test/embed_source.t/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names person_runner person2_runner person_thing_runner) 3 | (libraries pyml)) 4 | -------------------------------------------------------------------------------- /test/embed_source.t/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | -------------------------------------------------------------------------------- /test/embed_source.t/person.py: -------------------------------------------------------------------------------- 1 | class Person: 2 | def __init__(self, name, age): 3 | self.name = name 4 | self.age = age 5 | 6 | def __str__(self): 7 | return(f'Person -- name: {self.name}, age: {self.age}') 8 | -------------------------------------------------------------------------------- /test/embed_source.t/person2_runner.ml: -------------------------------------------------------------------------------- 1 | let () = Py.initialize () 2 | 3 | let hagrid = Person2.__init__ ~name:"Hagrid" ~age:111 () 4 | 5 | let () = print_endline @@ Person2.__str__ hagrid () 6 | -------------------------------------------------------------------------------- /test/embed_source.t/person_runner.ml: -------------------------------------------------------------------------------- 1 | let () = Py.initialize () 2 | 3 | let hagrid = Person.__init__ ~name:"Hagrid" ~age:111 () 4 | 5 | let () = print_endline @@ Person.__str__ hagrid () 6 | -------------------------------------------------------------------------------- /test/embed_source.t/person_specs.txt: -------------------------------------------------------------------------------- 1 | val __init__ : name:string -> age:int -> unit -> t 2 | val __str__ : t -> unit -> string 3 | -------------------------------------------------------------------------------- /test/embed_source.t/person_thing_runner.ml: -------------------------------------------------------------------------------- 1 | open Lib 2 | 3 | let () = Py.initialize () 4 | 5 | let hagrid = Person.__init__ ~name:"Hagrid" ~age:111 () 6 | 7 | let thing = Thing.__init__ ~color:"orange" () 8 | 9 | let () = print_endline @@ Person.__str__ hagrid () 10 | 11 | let () = print_endline @@ Thing.__str__ thing () 12 | -------------------------------------------------------------------------------- /test/embed_source.t/py/cool_package/person2.py: -------------------------------------------------------------------------------- 1 | class Person: 2 | def __init__(self, name, age): 3 | self.name = name 4 | self.age = age 5 | 6 | def __str__(self): 7 | return(f'Person -- name: {self.name}, age: {self.age}') 8 | -------------------------------------------------------------------------------- /test/embed_source.t/thing.py: -------------------------------------------------------------------------------- 1 | class Thing: 2 | """A thing is pretty basic. It does have a color though!""" 3 | 4 | def __init__(self, color): 5 | """Just to see if {| any |} characters mess it up.""" 6 | self.color = color 7 | 8 | def __str__(self): 9 | return(f'Thing -- color: {self.color}') 10 | -------------------------------------------------------------------------------- /test/embed_source.t/thing_specs.txt: -------------------------------------------------------------------------------- 1 | val __init__ : color:string -> unit -> t 2 | val __str__ : t -> unit -> string 3 | -------------------------------------------------------------------------------- /test/error_messages.t/.ocamlformat: -------------------------------------------------------------------------------- 1 | profile = default 2 | version = 0.23.0 3 | parse-docstrings = true 4 | wrap-comments = true 5 | -------------------------------------------------------------------------------- /test/error_messages.t/run.t: -------------------------------------------------------------------------------- 1 | When missing penultimate unit arg, you get a halfway decent error 2 | message. 3 | 4 | $ pyml_bindgen val_specs.txt na na | ocamlformat --name=a.ml - 5 | ("Error generating spec for 'val bad : t -> apple:int -> int'" 6 | ("Could not create py function from val_spec." 7 | "Val specs must specify attributes, instance, class, or module methods, or placeholders." 8 | "Unless you're defining an attribute, don't forget the penultimate unit argument!" 9 | ("The bad val_spec was:" 10 | ((ml_fun_name bad) 11 | (args 12 | ((Positional ((type_ T))) 13 | (Labeled ((ml_name apple) (py_name apple) (type_ Int))) 14 | (Positional ((type_ Int))))))))) 15 | -------------------------------------------------------------------------------- /test/error_messages.t/val_specs.txt: -------------------------------------------------------------------------------- 1 | val foo : t -> int 2 | val bad : t -> apple:int -> int 3 | val bar : a:int -> b:int -> unit -> int 4 | -------------------------------------------------------------------------------- /test/gen_multi.t/.ocamlformat: -------------------------------------------------------------------------------- 1 | profile = default 2 | version = 0.23.0 3 | parse-docstrings = true 4 | wrap-comments = true 5 | -------------------------------------------------------------------------------- /test/gen_multi.t/bad.tsv: -------------------------------------------------------------------------------- 1 | apple pie 2 | is good -------------------------------------------------------------------------------- /test/gen_multi.t/run.t: -------------------------------------------------------------------------------- 1 | Order of tsv file matters. 2 | 3 | $ gen_multi specs/cli_specs.tsv | grep '^module' 4 | module Human : sig 5 | module Cat : sig 6 | 7 | $ gen_multi specs/cli_specs_cat_first.tsv | grep '^module' 8 | module Cat : sig 9 | module Human : sig 10 | -------------------------------------------------------------------------------- /test/gen_multi.t/specs/cat_spec.txt: -------------------------------------------------------------------------------- 1 | val create : name:string -> unit -> t 2 | [@@py_fun_name __init__] 3 | 4 | val to_string : t -> unit -> string 5 | [@@py_fun_name __str__] 6 | 7 | val adopt_human : t -> human:Human.t -> unit -> unit 8 | 9 | val name : t -> string 10 | val human : t -> Human.t -------------------------------------------------------------------------------- /test/gen_multi.t/specs/cli_specs.tsv: -------------------------------------------------------------------------------- 1 | signatures py_module py_class associated_with caml_module split_caml_module embed_python_source of_pyo_ret_type 2 | specs/human_spec.txt human Human class Human NA NA no_check 3 | specs/cat_spec.txt cat Cat class Cat NA NA no_check 4 | -------------------------------------------------------------------------------- /test/gen_multi.t/specs/cli_specs_cat_first.tsv: -------------------------------------------------------------------------------- 1 | signatures py_module py_class associated_with caml_module split_caml_module embed_python_source of_pyo_ret_type 2 | specs/cat_spec.txt cat Cat class Cat NA NA no_check 3 | specs/human_spec.txt human Human class Human NA NA no_check 4 | -------------------------------------------------------------------------------- /test/gen_multi.t/specs/human_spec.txt: -------------------------------------------------------------------------------- 1 | val create : name:string -> unit -> t 2 | [@@py_fun_name __init__] 3 | 4 | val to_string : t -> unit -> string 5 | [@@py_fun_name __str__] 6 | 7 | val adopt_cat : t -> cat:Cat.t -> unit -> unit 8 | 9 | val name : t -> string 10 | val cat : t -> Cat.t -------------------------------------------------------------------------------- /test/helpers/sanitize_logs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sed -E 's/#[0-9]+/PID/g;s/[0-9]{2}:[0-9]{2}:[0-9]{2}/TIME/g;s/[0-9]{4}-[0-9]{2}-[0-9]{2}/DATE/g' "${1}" 4 | -------------------------------------------------------------------------------- /test/module_function_binding.t/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names run_no_caml_module run_with_caml_module) 3 | (libraries base pyml stdio)) 4 | -------------------------------------------------------------------------------- /test/module_function_binding.t/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | -------------------------------------------------------------------------------- /test/module_function_binding.t/run.t: -------------------------------------------------------------------------------- 1 | These are a little brittle...they will break if ocamlformat changes 2 | the way it formats, but it is so much easier to read the generated 3 | code if it is properly formatted. 4 | 5 | Setup env 6 | 7 | $ export SANITIZE_LOGS=$PWD/../helpers/sanitize_logs 8 | 9 | Run 10 | 11 | $ if [ -f lib.ml ]; then rm lib.ml; fi 12 | $ pyml_bindgen sigs_no_check.txt silly Silly -r no_check -a module > lib.ml 13 | $ ocamlformat --enable-outside-detected-project lib.ml 14 | let filter_opt l = List.filter_map Fun.id l 15 | let py_module = lazy (Py.Import.import_module "silly") 16 | let import_module () = Lazy.force py_module 17 | 18 | type t = Pytypes.pyobject 19 | 20 | let of_pyobject pyo = pyo 21 | let to_pyobject x = x 22 | 23 | let add ~a ~b () = 24 | let callable = Py.Module.get (import_module ()) "add" in 25 | let kwargs = 26 | filter_opt [ Some ("a", Py.Int.of_int a); Some ("b", Py.Int.of_int b) ] 27 | in 28 | Py.Int.to_int @@ Py.Callable.to_function_with_keywords callable [||] kwargs 29 | 30 | let do_nothing () = 31 | let callable = Py.Module.get (import_module ()) "do_nothing" in 32 | let kwargs = filter_opt [] in 33 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 34 | $ dune exec ./run_no_caml_module.exe 2> /dev/null 35 | add: 30 36 | 37 | Run 38 | 39 | $ if [ -f lib.ml ]; then rm lib.ml; fi 40 | $ pyml_bindgen sigs_no_check.txt silly Silly -c Silly -r no_check -a module > lib.ml 41 | $ ocamlformat --enable-outside-detected-project lib.ml 42 | module Silly : sig 43 | type t 44 | 45 | val of_pyobject : Pytypes.pyobject -> t 46 | val to_pyobject : t -> Pytypes.pyobject 47 | val add : a:int -> b:int -> unit -> int 48 | val do_nothing : unit -> unit 49 | end = struct 50 | let filter_opt l = List.filter_map Fun.id l 51 | let py_module = lazy (Py.Import.import_module "silly") 52 | let import_module () = Lazy.force py_module 53 | 54 | type t = Pytypes.pyobject 55 | 56 | let of_pyobject pyo = pyo 57 | let to_pyobject x = x 58 | 59 | let add ~a ~b () = 60 | let callable = Py.Module.get (import_module ()) "add" in 61 | let kwargs = 62 | filter_opt [ Some ("a", Py.Int.of_int a); Some ("b", Py.Int.of_int b) ] 63 | in 64 | Py.Int.to_int @@ Py.Callable.to_function_with_keywords callable [||] kwargs 65 | 66 | let do_nothing () = 67 | let callable = Py.Module.get (import_module ()) "do_nothing" in 68 | let kwargs = filter_opt [] in 69 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 70 | end 71 | $ dune exec ./run_with_caml_module.exe 72 | add: 30 73 | -------------------------------------------------------------------------------- /test/module_function_binding.t/run_no_caml_module.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Lib 3 | open Stdio 4 | 5 | let () = Py.initialize () 6 | 7 | let () = print_endline ("add: " ^ Int.to_string (add ~a:10 ~b:20 ())) 8 | 9 | let () = do_nothing () 10 | -------------------------------------------------------------------------------- /test/module_function_binding.t/run_with_caml_module.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Lib 3 | open Stdio 4 | 5 | let () = Py.initialize () 6 | 7 | let () = print_endline ("add: " ^ Int.to_string (Silly.add ~a:10 ~b:20 ())) 8 | 9 | let () = Silly.do_nothing () 10 | -------------------------------------------------------------------------------- /test/module_function_binding.t/sigs_no_check.txt: -------------------------------------------------------------------------------- 1 | val add : a:int -> b:int -> unit -> int 2 | val do_nothing : unit -> unit 3 | -------------------------------------------------------------------------------- /test/module_function_binding.t/silly.py: -------------------------------------------------------------------------------- 1 | def add(a, b): 2 | return a + b 3 | 4 | def do_nothing(): 5 | return None 6 | -------------------------------------------------------------------------------- /test/nested_submodules.t/creature.ml: -------------------------------------------------------------------------------- 1 | module Bug = struct 2 | module Fly : sig 3 | type t 4 | 5 | val of_pyobject : Pytypes.pyobject -> t 6 | 7 | val to_pyobject : t -> Pytypes.pyobject 8 | 9 | val make : string -> t 10 | end = struct 11 | type t = string 12 | 13 | let of_pyobject = Py.String.to_string 14 | 15 | let to_pyobject = Py.String.of_string 16 | 17 | let make s = s 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/nested_submodules.t/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names run_no_check run_option run_or_error) 3 | (libraries base pyml stdio) 4 | (preprocess (pps ppx_sexp_conv))) 5 | -------------------------------------------------------------------------------- /test/nested_submodules.t/run.t: -------------------------------------------------------------------------------- 1 | These are a little brittle...they will break if ocamlformat changes 2 | the way it formats, but it is so much easier to read the generated 3 | code if it is properly formatted. 4 | 5 | Setup env 6 | 7 | $ export SANITIZE_LOGS=$PWD/../helpers/sanitize_logs 8 | 9 | of_pyobject with no check 10 | 11 | $ if [ -f lib.ml ]; then rm lib.ml; fi 12 | $ pyml_bindgen specs_no_check.txt silly Cat --caml-module=Cat --of-pyo-ret-type=no_check > lib.ml 13 | $ ocamlformat --enable-outside-detected-project lib.ml 14 | module Cat : sig 15 | type t 16 | 17 | val of_pyobject : Pytypes.pyobject -> t 18 | val to_pyobject : t -> Pytypes.pyobject 19 | val __init__ : name:string -> unit -> t 20 | val eat : t -> fly:Creature.Bug.Fly.t -> unit -> unit 21 | val hunger : t -> int 22 | end = struct 23 | let filter_opt l = List.filter_map Fun.id l 24 | let py_module = lazy (Py.Import.import_module "silly") 25 | let import_module () = Lazy.force py_module 26 | 27 | type t = Pytypes.pyobject 28 | 29 | let of_pyobject pyo = pyo 30 | let to_pyobject x = x 31 | 32 | let __init__ ~name () = 33 | let callable = Py.Module.get (import_module ()) "Cat" in 34 | let kwargs = filter_opt [ Some ("name", Py.String.of_string name) ] in 35 | of_pyobject @@ Py.Callable.to_function_with_keywords callable [||] kwargs 36 | 37 | let eat t ~fly () = 38 | let callable = Py.Object.find_attr_string t "eat" in 39 | let kwargs = 40 | filter_opt [ Some ("fly", Creature.Bug.Fly.to_pyobject fly) ] 41 | in 42 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 43 | 44 | let hunger t = Py.Int.to_int @@ Py.Object.find_attr_string t "hunger" 45 | end 46 | $ dune exec ./run_no_check.exe 2> /dev/null 47 | 10 48 | 5 49 | done 50 | 51 | of_pyobject returning option 52 | 53 | $ if [ -f lib.ml ]; then rm lib.ml; fi 54 | $ pyml_bindgen specs_option.txt silly Cat --caml-module=Cat --of-pyo-ret-type=option > lib.ml 55 | $ ocamlformat --enable-outside-detected-project lib.ml 56 | module Cat : sig 57 | type t 58 | 59 | val of_pyobject : Pytypes.pyobject -> t option 60 | val to_pyobject : t -> Pytypes.pyobject 61 | val __init__ : name:string -> unit -> t option 62 | val eat : t -> fly:Creature.Bug.Fly.t -> unit -> unit 63 | val hunger : t -> int 64 | end = struct 65 | let filter_opt l = List.filter_map Fun.id l 66 | let py_module = lazy (Py.Import.import_module "silly") 67 | let import_module () = Lazy.force py_module 68 | 69 | type t = Pytypes.pyobject 70 | 71 | let is_instance pyo = 72 | let py_class = Py.Module.get (import_module ()) "Cat" in 73 | Py.Object.is_instance pyo py_class 74 | 75 | let of_pyobject pyo = if is_instance pyo then Some pyo else None 76 | let to_pyobject x = x 77 | 78 | let __init__ ~name () = 79 | let callable = Py.Module.get (import_module ()) "Cat" in 80 | let kwargs = filter_opt [ Some ("name", Py.String.of_string name) ] in 81 | of_pyobject @@ Py.Callable.to_function_with_keywords callable [||] kwargs 82 | 83 | let eat t ~fly () = 84 | let callable = Py.Object.find_attr_string t "eat" in 85 | let kwargs = 86 | filter_opt [ Some ("fly", Creature.Bug.Fly.to_pyobject fly) ] 87 | in 88 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 89 | 90 | let hunger t = Py.Int.to_int @@ Py.Object.find_attr_string t "hunger" 91 | end 92 | $ dune exec ./run_option.exe 2> /dev/null 93 | 10 94 | 5 95 | done 96 | 97 | of_pyobject returning Or_error 98 | 99 | $ if [ -f lib.ml ]; then rm lib.ml; fi 100 | $ pyml_bindgen specs_or_error.txt silly Cat --caml-module=Cat --of-pyo-ret-type=or_error > lib.ml 101 | $ ocamlformat --enable-outside-detected-project lib.ml 102 | open! Base 103 | 104 | module Cat : sig 105 | type t 106 | 107 | val of_pyobject : Pytypes.pyobject -> t Or_error.t 108 | val to_pyobject : t -> Pytypes.pyobject 109 | val __init__ : name:string -> unit -> t Or_error.t 110 | val eat : t -> fly:Creature.Bug.Fly.t -> unit -> unit 111 | val hunger : t -> int 112 | end = struct 113 | let filter_opt = List.filter_opt 114 | let py_module = lazy (Py.Import.import_module "silly") 115 | let import_module () = Lazy.force py_module 116 | 117 | type t = Pytypes.pyobject 118 | 119 | let is_instance pyo = 120 | let py_class = Py.Module.get (import_module ()) "Cat" in 121 | Py.Object.is_instance pyo py_class 122 | 123 | let of_pyobject pyo = 124 | if is_instance pyo then Or_error.return pyo 125 | else Or_error.error_string "Expected Cat" 126 | 127 | let to_pyobject x = x 128 | 129 | let __init__ ~name () = 130 | let callable = Py.Module.get (import_module ()) "Cat" in 131 | let kwargs = filter_opt [ Some ("name", Py.String.of_string name) ] in 132 | of_pyobject @@ Py.Callable.to_function_with_keywords callable [||] kwargs 133 | 134 | let eat t ~fly () = 135 | let callable = Py.Object.find_attr_string t "eat" in 136 | let kwargs = 137 | filter_opt [ Some ("fly", Creature.Bug.Fly.to_pyobject fly) ] 138 | in 139 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 140 | 141 | let hunger t = Py.Int.to_int @@ Py.Object.find_attr_string t "hunger" 142 | end 143 | $ dune exec ./run_or_error.exe 2> /dev/null 144 | 10 145 | 5 146 | done 147 | -------------------------------------------------------------------------------- /test/nested_submodules.t/run_no_check.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Lib 3 | open Stdio 4 | 5 | let () = Py.initialize () 6 | 7 | let cat = Cat.__init__ ~name:"Sam" () 8 | 9 | let () = print_endline @@ Int.to_string @@ Cat.hunger cat 10 | 11 | let fly = Creature.Bug.Fly.make "Bill" 12 | 13 | let () = Cat.eat cat ~fly () 14 | 15 | let () = print_endline @@ Int.to_string @@ Cat.hunger cat 16 | 17 | let () = print_endline "done" 18 | -------------------------------------------------------------------------------- /test/nested_submodules.t/run_option.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Lib 3 | open Stdio 4 | 5 | let () = Py.initialize () 6 | 7 | let cat = Option.value_exn (Cat.__init__ ~name:"Sam" ()) 8 | 9 | let () = print_endline @@ Int.to_string @@ Cat.hunger cat 10 | 11 | let fly = Creature.Bug.Fly.make "Bill" 12 | 13 | let () = Cat.eat cat ~fly () 14 | 15 | let () = print_endline @@ Int.to_string @@ Cat.hunger cat 16 | 17 | let () = print_endline "done" 18 | -------------------------------------------------------------------------------- /test/nested_submodules.t/run_or_error.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Lib 3 | open Stdio 4 | 5 | let () = Py.initialize () 6 | 7 | let cat = Or_error.ok_exn (Cat.__init__ ~name:"Sam" ()) 8 | 9 | let () = print_endline @@ Int.to_string @@ Cat.hunger cat 10 | 11 | let fly = Creature.Bug.Fly.make "Bill" 12 | 13 | let () = Cat.eat cat ~fly () 14 | 15 | let () = print_endline @@ Int.to_string @@ Cat.hunger cat 16 | 17 | let () = print_endline "done" 18 | -------------------------------------------------------------------------------- /test/nested_submodules.t/silly.py: -------------------------------------------------------------------------------- 1 | class Cat: 2 | def __init__(self, name): 3 | self.name = name 4 | self.hunger = 10 5 | 6 | def eat(self, fly): 7 | # fly is pointless here, but its just an example :) 8 | self.hunger -= 5 9 | 10 | -------------------------------------------------------------------------------- /test/nested_submodules.t/specs_no_check.txt: -------------------------------------------------------------------------------- 1 | val __init__ : name:string -> unit -> t 2 | val eat : t -> fly:Creature.Bug.Fly.t -> unit -> unit 3 | val hunger : t -> int 4 | -------------------------------------------------------------------------------- /test/nested_submodules.t/specs_option.txt: -------------------------------------------------------------------------------- 1 | val __init__ : name:string -> unit -> t option 2 | val eat : t -> fly:Creature.Bug.Fly.t -> unit -> unit 3 | val hunger : t -> int 4 | -------------------------------------------------------------------------------- /test/nested_submodules.t/specs_or_error.txt: -------------------------------------------------------------------------------- 1 | val __init__ : name:string -> unit -> t Or_error.t 2 | val eat : t -> fly:Creature.Bug.Fly.t -> unit -> unit 3 | val hunger : t -> int 4 | -------------------------------------------------------------------------------- /test/of_pyo_no_check.t/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name hi) 3 | (libraries pyml)) 4 | -------------------------------------------------------------------------------- /test/of_pyo_no_check.t/hi.ml: -------------------------------------------------------------------------------- 1 | open Lib 2 | 3 | let () = Py.initialize () 4 | 5 | let () = print_endline @@ Silly.x @@ Silly.__init__ ~x:"all good!" () 6 | -------------------------------------------------------------------------------- /test/of_pyo_no_check.t/run.t: -------------------------------------------------------------------------------- 1 | Setup env 2 | 3 | $ export SANITIZE_LOGS=$PWD/../helpers/sanitize_logs 4 | 5 | Basic usage. 6 | 7 | $ if [ -f lib.ml ]; then rm lib.ml; fi 8 | $ pyml_bindgen silly_sigs_no_check.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=no_check > lib.ml 9 | $ dune exec ./hi.exe 2> /dev/null 10 | all good! 11 | 12 | Sigs with t option, but requesting no check work, but give compiler 13 | error if you try to run the code. 14 | 15 | $ if [ -f lib.ml ]; then rm lib.ml; fi 16 | $ pyml_bindgen silly_sigs_option.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=no_check > lib.ml 17 | $ dune exec ./hi.exe 2> err 18 | [1] 19 | $ grep -i -q 'signature mismatch' err 20 | 21 | Sigs with t Or_error.t, but requesting no check fail. 22 | 23 | $ if [ -f lib.ml ]; then rm lib.ml; fi 24 | $ pyml_bindgen silly_sigs_or_error.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=no_check > lib.ml 2> err 25 | [1] 26 | $ bash "${SANITIZE_LOGS}" err 27 | ERROR: You said you wanted No_check return type, but Or_error was found in the sigs. 28 | 29 | Mixed return types will give if you try to run the code. pyml_bindgen 30 | program will run fine however... 31 | 32 | $ if [ -f lib.ml ]; then rm lib.ml; fi 33 | $ pyml_bindgen silly_sigs_no_check_option.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=no_check > lib.ml 34 | $ dune exec ./hi.exe 2> err 35 | [1] 36 | $ grep -i -q 'signature mismatch' err 37 | 38 | If Or_error is present in the signatures but not passed correct 39 | --of-pyo-ret-type, it's an error. 40 | 41 | $ if [ -f lib.ml ]; then rm lib.ml; fi 42 | $ pyml_bindgen silly_sigs_no_check_or_error.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=no_check > lib.ml 2> err 43 | [1] 44 | $ bash "${SANITIZE_LOGS}" err 45 | ERROR: You said you wanted No_check return type, but Or_error was found in the sigs. 46 | 47 | 48 | $ if [ -f lib.ml ]; then rm lib.ml; fi 49 | $ pyml_bindgen silly_sigs_option_or_error.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=no_check > lib.ml 2> err 50 | [1] 51 | $ bash "${SANITIZE_LOGS}" err 52 | ERROR: You said you wanted No_check return type, but Or_error was found in the sigs. 53 | -------------------------------------------------------------------------------- /test/of_pyo_no_check.t/silly_mod.py: -------------------------------------------------------------------------------- 1 | class Silly: 2 | def __init__(self, x): 3 | self.x = x 4 | -------------------------------------------------------------------------------- /test/of_pyo_no_check.t/silly_sigs_no_check.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t 2 | val x : t -> string 3 | -------------------------------------------------------------------------------- /test/of_pyo_no_check.t/silly_sigs_no_check_option.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t 2 | val x : t -> string 3 | val bad_fun : x:string -> unit -> t option 4 | -------------------------------------------------------------------------------- /test/of_pyo_no_check.t/silly_sigs_no_check_or_error.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t 2 | val x : t -> string 3 | val bad_fun : x:string -> unit -> t Or_error.t 4 | -------------------------------------------------------------------------------- /test/of_pyo_no_check.t/silly_sigs_option.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t option 2 | val x : t -> string 3 | -------------------------------------------------------------------------------- /test/of_pyo_no_check.t/silly_sigs_option_or_error.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t option 2 | val x : t -> string 3 | val bad_fun : x:string -> unit -> t Or_error.t 4 | -------------------------------------------------------------------------------- /test/of_pyo_no_check.t/silly_sigs_or_error.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t Or_error.t 2 | val x : t -> string 3 | -------------------------------------------------------------------------------- /test/of_pyo_option.t/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name hi) 3 | (libraries pyml)) 4 | -------------------------------------------------------------------------------- /test/of_pyo_option.t/hi.ml: -------------------------------------------------------------------------------- 1 | open Lib 2 | 3 | let () = Py.initialize () 4 | 5 | let () = 6 | match Silly.__init__ ~x:"all good!" () with 7 | | Some s -> print_endline @@ Silly.x s 8 | | None -> prerr_endline "Couldn't make a Silly thing" 9 | -------------------------------------------------------------------------------- /test/of_pyo_option.t/run.t: -------------------------------------------------------------------------------- 1 | Setup env 2 | 3 | $ export SANITIZE_LOGS=$PWD/../helpers/sanitize_logs 4 | 5 | Basic usage. 6 | 7 | $ if [ -f lib.ml ]; then rm lib.ml; fi 8 | $ pyml_bindgen silly_sigs_option.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=option > lib.ml 9 | $ dune exec ./hi.exe 2> /dev/null 10 | all good! 11 | 12 | 13 | Sigs with t no_check, but requesting option work, but give compiler 14 | error if you try to run the code. 15 | 16 | $ if [ -f lib.ml ]; then rm lib.ml; fi 17 | $ pyml_bindgen silly_sigs_no_check.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=option > lib.ml 18 | $ dune exec ./hi.exe 2> err 19 | [1] 20 | $ grep -i -q 'signature mismatch' err 21 | 22 | Sigs with t Or_error.t, but requesting option fail. 23 | 24 | $ if [ -f lib.ml ]; then rm lib.ml; fi 25 | $ pyml_bindgen silly_sigs_or_error.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=option > lib.ml 2> err 26 | [1] 27 | $ bash "${SANITIZE_LOGS}" err 28 | ERROR: You said you wanted Option return type, but Or_error was found in the sigs. 29 | 30 | 31 | Mixed return types will give if you try to run the code. pyml_bindgen 32 | program will run fine however... 33 | 34 | $ if [ -f lib.ml ]; then rm lib.ml; fi 35 | $ pyml_bindgen silly_sigs_no_check_option.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=option > lib.ml 36 | $ dune exec ./hi.exe 2> err 37 | [1] 38 | $ grep -i -q 'signature mismatch' err 39 | 40 | If Or_error is present in the signatures but not passed correct 41 | --of-pyo-ret-type, it's an error. 42 | 43 | $ if [ -f lib.ml ]; then rm lib.ml; fi 44 | $ pyml_bindgen silly_sigs_no_check_or_error.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=option > lib.ml 2> err 45 | [1] 46 | $ bash "${SANITIZE_LOGS}" err 47 | ERROR: You said you wanted Option return type, but Or_error was found in the sigs. 48 | 49 | $ if [ -f lib.ml ]; then rm lib.ml; fi 50 | $ pyml_bindgen silly_sigs_option_or_error.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=option > lib.ml 2> err 51 | [1] 52 | $ bash "${SANITIZE_LOGS}" err 53 | ERROR: You said you wanted Option return type, but Or_error was found in the sigs. 54 | -------------------------------------------------------------------------------- /test/of_pyo_option.t/silly_mod.py: -------------------------------------------------------------------------------- 1 | class Silly: 2 | def __init__(self, x): 3 | self.x = x 4 | -------------------------------------------------------------------------------- /test/of_pyo_option.t/silly_sigs_no_check.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t 2 | val x : t -> string 3 | -------------------------------------------------------------------------------- /test/of_pyo_option.t/silly_sigs_no_check_option.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t 2 | val x : t -> string 3 | val bad_fun : x:string -> unit -> t option 4 | -------------------------------------------------------------------------------- /test/of_pyo_option.t/silly_sigs_no_check_or_error.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t 2 | val x : t -> string 3 | val bad_fun : x:string -> unit -> t Or_error.t 4 | -------------------------------------------------------------------------------- /test/of_pyo_option.t/silly_sigs_option.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t option 2 | val x : t -> string 3 | -------------------------------------------------------------------------------- /test/of_pyo_option.t/silly_sigs_option_or_error.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t option 2 | val x : t -> string 3 | val bad_fun : x:string -> unit -> t Or_error.t 4 | -------------------------------------------------------------------------------- /test/of_pyo_option.t/silly_sigs_or_error.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t Or_error.t 2 | val x : t -> string 3 | -------------------------------------------------------------------------------- /test/of_pyo_or_error.t/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name hi) 3 | (libraries base pyml)) 4 | -------------------------------------------------------------------------------- /test/of_pyo_or_error.t/hi.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Lib 3 | 4 | let () = Py.initialize () 5 | 6 | let () = 7 | Caml.print_endline @@ Silly.x @@ Or_error.ok_exn 8 | @@ Silly.__init__ ~x:"all good!" () 9 | -------------------------------------------------------------------------------- /test/of_pyo_or_error.t/run.t: -------------------------------------------------------------------------------- 1 | Setup env 2 | 3 | $ export SANITIZE_LOGS=$PWD/../helpers/sanitize_logs 4 | 5 | Basic usage. 6 | 7 | $ if [ -f lib.ml ]; then rm lib.ml; fi 8 | $ pyml_bindgen silly_sigs_or_error.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=or_error > lib.ml 9 | $ dune exec ./hi.exe 2> /dev/null 10 | all good! 11 | 12 | Sigs with t no_check, but requesting or_error fail. 13 | 14 | $ if [ -f lib.ml ]; then rm lib.ml; fi 15 | $ pyml_bindgen silly_sigs_no_check.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=or_error > lib.ml 2> err 16 | [1] 17 | $ bash "${SANITIZE_LOGS}" err 18 | ERROR: You said you wanted Or_error return type, but Or_error was not found in the sigs. 19 | 20 | Sigs with t option, but requesting or_error fail. 21 | 22 | $ if [ -f lib.ml ]; then rm lib.ml; fi 23 | $ pyml_bindgen silly_sigs_option.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=or_error > lib.ml 2> err 24 | [1] 25 | $ bash "${SANITIZE_LOGS}" err 26 | ERROR: You said you wanted Or_error return type, but Or_error was not found in the sigs. 27 | 28 | Mixed return types will fail. 29 | 30 | $ if [ -f lib.ml ]; then rm lib.ml; fi 31 | $ pyml_bindgen silly_sigs_no_check_option.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=or_error > lib.ml 2> err 32 | [1] 33 | $ bash "${SANITIZE_LOGS}" err 34 | ERROR: You said you wanted Or_error return type, but Or_error was not found in the sigs. 35 | 36 | If you have no_check or option mixed in with or error, and you request 37 | or_error, pyml_bindgen will NOT fail. But compiling the result, WILL 38 | fail. 39 | 40 | $ if [ -f lib.ml ]; then rm lib.ml; fi 41 | $ pyml_bindgen silly_sigs_no_check_or_error.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=or_error > lib.ml 42 | $ dune exec ./hi.exe 2> err 43 | [1] 44 | $ grep -i -q 'signature mismatch' err 45 | 46 | $ if [ -f lib.ml ]; then rm lib.ml; fi 47 | $ pyml_bindgen silly_sigs_option_or_error.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=or_error > lib.ml 48 | $ dune exec ./hi.exe 2> err 49 | [1] 50 | $ grep -i -q 'signature mismatch' err 51 | -------------------------------------------------------------------------------- /test/of_pyo_or_error.t/silly_mod.py: -------------------------------------------------------------------------------- 1 | class Silly: 2 | def __init__(self, x): 3 | self.x = x 4 | -------------------------------------------------------------------------------- /test/of_pyo_or_error.t/silly_sigs_no_check.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t 2 | val x : t -> string 3 | -------------------------------------------------------------------------------- /test/of_pyo_or_error.t/silly_sigs_no_check_option.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t 2 | val x : t -> string 3 | val bad_fun : x:string -> unit -> t option 4 | -------------------------------------------------------------------------------- /test/of_pyo_or_error.t/silly_sigs_no_check_or_error.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t 2 | val x : t -> string 3 | val bad_fun : x:string -> unit -> t Or_error.t 4 | -------------------------------------------------------------------------------- /test/of_pyo_or_error.t/silly_sigs_option.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t option 2 | val x : t -> string 3 | -------------------------------------------------------------------------------- /test/of_pyo_or_error.t/silly_sigs_option_or_error.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t option 2 | val x : t -> string 3 | val bad_fun : x:string -> unit -> t Or_error.t 4 | -------------------------------------------------------------------------------- /test/of_pyo_or_error.t/silly_sigs_or_error.txt: -------------------------------------------------------------------------------- 1 | val __init__ : x:string -> unit -> t Or_error.t 2 | val x : t -> string 3 | -------------------------------------------------------------------------------- /test/old_bugs.t/pr_value_bug.txt: -------------------------------------------------------------------------------- 1 | # pr_value got parsed as pr_, val, ue like that by the binary, but not the lib code. 2 | val add_feature : t -> pr_name:string -> pr_value:string -> unit -> unit 3 | val add_feature2 : t -> pr_name:string -> pr_value:string -> unit -> unit 4 | val add_feature3 : t -> pr_name:string -> pr_value:string -> unit -> unit 5 | -------------------------------------------------------------------------------- /test/old_bugs.t/run.t: -------------------------------------------------------------------------------- 1 | Setup env 2 | 3 | $ export SANITIZE_LOGS=$PWD/../helpers/sanitize_logs 4 | 5 | Basic usage. 6 | 7 | $ if [ -f lib.ml ]; then rm lib.ml; fi 8 | $ pyml_bindgen pr_value_bug.txt silly_mod Silly --caml-module=Silly --of-pyo-ret-type=no_check > lib.ml 9 | $ ocamlformat --enable --name=a.ml lib.ml 10 | module Silly : sig 11 | type t 12 | 13 | val of_pyobject : Pytypes.pyobject -> t 14 | val to_pyobject : t -> Pytypes.pyobject 15 | val add_feature : t -> pr_name:string -> pr_value:string -> unit -> unit 16 | val add_feature2 : t -> pr_name:string -> pr_value:string -> unit -> unit 17 | val add_feature3 : t -> pr_name:string -> pr_value:string -> unit -> unit 18 | end = struct 19 | let filter_opt l = List.filter_map Fun.id l 20 | let py_module = lazy (Py.Import.import_module "silly_mod") 21 | let import_module () = Lazy.force py_module 22 | 23 | type t = Pytypes.pyobject 24 | 25 | let of_pyobject pyo = pyo 26 | let to_pyobject x = x 27 | 28 | let add_feature t ~pr_name ~pr_value () = 29 | let callable = Py.Object.find_attr_string t "add_feature" in 30 | let kwargs = 31 | filter_opt 32 | [ 33 | Some ("pr_name", Py.String.of_string pr_name); 34 | Some ("pr_value", Py.String.of_string pr_value); 35 | ] 36 | in 37 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 38 | 39 | let add_feature2 t ~pr_name ~pr_value () = 40 | let callable = Py.Object.find_attr_string t "add_feature2" in 41 | let kwargs = 42 | filter_opt 43 | [ 44 | Some ("pr_name", Py.String.of_string pr_name); 45 | Some ("pr_value", Py.String.of_string pr_value); 46 | ] 47 | in 48 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 49 | 50 | let add_feature3 t ~pr_name ~pr_value () = 51 | let callable = Py.Object.find_attr_string t "add_feature3" in 52 | let kwargs = 53 | filter_opt 54 | [ 55 | Some ("pr_name", Py.String.of_string pr_name); 56 | Some ("pr_value", Py.String.of_string pr_value); 57 | ] 58 | in 59 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 60 | end 61 | -------------------------------------------------------------------------------- /test/py_fun_name_attr.t/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names run) 3 | (libraries base pyml stdio)) 4 | -------------------------------------------------------------------------------- /test/py_fun_name_attr.t/run.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Lib 3 | open Stdio 4 | 5 | let () = Py.initialize () 6 | 7 | let cat = Cat.create ~name:"Sam" () 8 | 9 | let () = Cat.climb cat ~how_high:20 () 10 | 11 | let () = Cat.eat_part cat ~num_mice:0.2 () 12 | 13 | let () = Cat.eat cat ~num_mice:2 () 14 | 15 | let () = print_endline @@ Cat.to_string cat () 16 | 17 | let () = print_endline "done" 18 | -------------------------------------------------------------------------------- /test/py_fun_name_attr.t/run.t: -------------------------------------------------------------------------------- 1 | These are a little brittle...they will break if ocamlformat changes 2 | the way it formats, but it is so much easier to read the generated 3 | code if it is properly formatted. 4 | 5 | Setup env 6 | 7 | $ export SANITIZE_LOGS=$PWD/../helpers/sanitize_logs 8 | 9 | of_pyobject with no check 10 | 11 | $ if [ -f lib.ml ]; then rm lib.ml; fi 12 | $ pyml_bindgen specs.txt silly Cat --caml-module=Cat --of-pyo-ret-type=no_check > lib.ml 13 | $ ocamlformat --enable-outside-detected-project lib.ml 14 | module Cat : sig 15 | type t 16 | 17 | val of_pyobject : Pytypes.pyobject -> t 18 | val to_pyobject : t -> Pytypes.pyobject 19 | val create : name:string -> unit -> t 20 | val to_string : t -> unit -> string 21 | val eat : t -> num_mice:int -> unit -> unit 22 | val eat_part : t -> num_mice:float -> unit -> unit 23 | val climb : t -> how_high:int -> unit -> unit 24 | end = struct 25 | let filter_opt l = List.filter_map Fun.id l 26 | let py_module = lazy (Py.Import.import_module "silly") 27 | let import_module () = Lazy.force py_module 28 | 29 | type t = Pytypes.pyobject 30 | 31 | let of_pyobject pyo = pyo 32 | let to_pyobject x = x 33 | 34 | let create ~name () = 35 | let callable = Py.Module.get (import_module ()) "Cat" in 36 | let kwargs = filter_opt [ Some ("name", Py.String.of_string name) ] in 37 | of_pyobject @@ Py.Callable.to_function_with_keywords callable [||] kwargs 38 | 39 | let to_string t () = 40 | let callable = Py.Object.find_attr_string t "__str__" in 41 | let kwargs = filter_opt [] in 42 | Py.String.to_string 43 | @@ Py.Callable.to_function_with_keywords callable [||] kwargs 44 | 45 | let eat t ~num_mice () = 46 | let callable = Py.Object.find_attr_string t "eat" in 47 | let kwargs = filter_opt [ Some ("num_mice", Py.Int.of_int num_mice) ] in 48 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 49 | 50 | let eat_part t ~num_mice () = 51 | let callable = Py.Object.find_attr_string t "eat" in 52 | let kwargs = filter_opt [ Some ("num_mice", Py.Float.of_float num_mice) ] in 53 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 54 | 55 | let climb t ~how_high () = 56 | let callable = Py.Object.find_attr_string t "jump" in 57 | let kwargs = filter_opt [ Some ("how_high", Py.Int.of_int how_high) ] in 58 | ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs 59 | end 60 | $ dune exec ./run.exe 2> /dev/null 61 | Cat -- name: Sam, hunger: 9.0 62 | done 63 | -------------------------------------------------------------------------------- /test/py_fun_name_attr.t/silly.py: -------------------------------------------------------------------------------- 1 | class Cat: 2 | def __init__(self, name): 3 | self.name = name 4 | self.hunger = 0 5 | 6 | def __str__(self): 7 | return(f'Cat -- name: {self.name}, hunger: {self.hunger}') 8 | 9 | def eat(self, num_mice=1): 10 | self.hunger -= (num_mice * 5) 11 | if self.hunger < 0: 12 | self.hunger = 0 13 | 14 | def jump(self, how_high=1): 15 | if how_high > 0: 16 | self.hunger += how_high 17 | -------------------------------------------------------------------------------- /test/py_fun_name_attr.t/specs.txt: -------------------------------------------------------------------------------- 1 | val create : name:string -> unit -> t 2 | [@@py_fun_name __init__] 3 | val to_string : t -> unit -> string 4 | [@@py_fun_name __str__] 5 | 6 | val eat : t -> num_mice:int -> unit -> unit 7 | val eat_part : t -> num_mice:float -> unit -> unit 8 | [@@py_fun_name eat] 9 | 10 | val climb : t -> how_high:int -> unit -> unit 11 | [@@py_fun_name jump] 12 | -------------------------------------------------------------------------------- /test/raw_pyobjects.t/creature.ml: -------------------------------------------------------------------------------- 1 | module Bug = struct 2 | module Fly : sig 3 | type t 4 | 5 | val of_pyobject : Pytypes.pyobject -> t 6 | 7 | val to_pyobject : t -> Pytypes.pyobject 8 | 9 | val make : string -> t 10 | end = struct 11 | type t = string 12 | 13 | let of_pyobject = Py.String.to_string 14 | 15 | let to_pyobject = Py.String.of_string 16 | 17 | let make s = s 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/raw_pyobjects.t/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name run) 3 | (libraries base pyml stdio) 4 | (preprocess (pps ppx_sexp_conv))) 5 | -------------------------------------------------------------------------------- /test/raw_pyobjects.t/run.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Lib 3 | open Stdio 4 | 5 | let () = Py.initialize () 6 | 7 | let () = 8 | print_endline @@ Int.to_string @@ Py.Int.to_int 9 | @@ Silly.concat1 ~x:(Py.Int.of_int 1) ~y:(Py.Int.of_int 2) () 10 | 11 | let () = 12 | print_endline @@ Int.to_string @@ Py.Int.to_int 13 | @@ Silly.concat2 ~x:(Py.Int.of_int 1) ~y:(Py.Int.of_int 2) () 14 | 15 | let () = 16 | print_endline @@ Py.String.to_string 17 | @@ Silly.concat1 18 | ~x:(Py.String.of_string "apple ") 19 | ~y:(Py.String.of_string "pie") 20 | () 21 | 22 | let () = 23 | print_endline @@ Py.String.to_string 24 | @@ Silly.concat2 25 | ~x:(Py.String.of_string "apple ") 26 | ~y:(Py.String.of_string "pie") 27 | () 28 | 29 | let () = 30 | print_endline @@ Int.to_string @@ Silly.concat3 ~x:1 ~y:(Py.Int.of_int 2) () 31 | 32 | let () = print_endline "Watch out for type errors...." 33 | 34 | let () = 35 | print_s @@ [%sexp_of: string Or_error.t] 36 | @@ Or_error.try_with (fun () -> 37 | Py.String.to_string 38 | @@ Silly.concat2 ~x:(Py.Int.of_int 1) 39 | ~y:(Py.String.of_string " pie") 40 | ()) 41 | 42 | let () = print_endline "done" 43 | -------------------------------------------------------------------------------- /test/raw_pyobjects.t/silly.py: -------------------------------------------------------------------------------- 1 | def concat1(x, y): 2 | return(x + y) 3 | 4 | def concat2(x, y): 5 | return(x + y) 6 | 7 | def concat3(x, y): 8 | return(x + y) 9 | -------------------------------------------------------------------------------- /test/raw_pyobjects.t/specs.txt: -------------------------------------------------------------------------------- 1 | val concat1 : x:Pytypes.pyobject -> y:Pytypes.pyobject -> unit -> Pytypes.pyobject 2 | val concat2 : x:Py.Object.t -> y:Py.Object.t -> unit -> Py.Object.t 3 | val concat3 : x:int -> y:Py.Object.t -> unit -> int 4 | -------------------------------------------------------------------------------- /test/split_caml_module.t/.ocamlformat: -------------------------------------------------------------------------------- 1 | profile = default 2 | version = 0.23.0 3 | parse-docstrings = true 4 | wrap-comments = true 5 | -------------------------------------------------------------------------------- /test/split_caml_module.t/dune: -------------------------------------------------------------------------------- 1 | (executable (name hello) (libraries pyml)) 2 | -------------------------------------------------------------------------------- /test/split_caml_module.t/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | -------------------------------------------------------------------------------- /test/split_caml_module.t/hello.ml: -------------------------------------------------------------------------------- 1 | let () = Py.initialize () 2 | 3 | let () = 4 | let s = 5 | match Thing.create ~name:"Ryan" () with 6 | | None -> "oops!" 7 | | Some thing -> Thing.name thing 8 | in 9 | print_endline s 10 | -------------------------------------------------------------------------------- /test/split_caml_module.t/run.t: -------------------------------------------------------------------------------- 1 | With trailing / at end of path. 2 | 3 | $ pyml_bindgen specs.txt thing Thing --caml-module Thing --split-caml-module a/b/c/ 4 | $ ocamlformat a/b/c/thing.ml 5 | let filter_opt l = List.filter_map Fun.id l 6 | let py_module = lazy (Py.Import.import_module "thing") 7 | let import_module () = Lazy.force py_module 8 | 9 | type t = Pytypes.pyobject 10 | 11 | let is_instance pyo = 12 | let py_class = Py.Module.get (import_module ()) "Thing" in 13 | Py.Object.is_instance pyo py_class 14 | 15 | let of_pyobject pyo = if is_instance pyo then Some pyo else None 16 | let to_pyobject x = x 17 | 18 | let create ~name () = 19 | let callable = Py.Module.get (import_module ()) "Thing" in 20 | let kwargs = filter_opt [ Some ("name", Py.String.of_string name) ] in 21 | of_pyobject @@ Py.Callable.to_function_with_keywords callable [||] kwargs 22 | 23 | let name t = Py.String.to_string @@ Py.Object.find_attr_string t "name" 24 | $ ocamlformat a/b/c/thing.mli 25 | type t 26 | 27 | val of_pyobject : Pytypes.pyobject -> t option 28 | val to_pyobject : t -> Pytypes.pyobject 29 | val create : name:string -> unit -> t option 30 | val name : t -> string 31 | 32 | 33 | Without trailing / at end of path. 34 | 35 | $ pyml_bindgen specs.txt thing Thing --caml-module Thing --split-caml-module e/f/g 36 | $ diff a/b/c/thing.ml e/f/g/thing.ml 37 | $ diff a/b/c/thing.mli e/f/g/thing.mli 38 | 39 | Actually using it. 40 | 41 | $ pyml_bindgen specs.txt thing Thing --caml-module Thing --split-caml-module . 42 | $ dune exec ./hello.exe 43 | Ryan 44 | -------------------------------------------------------------------------------- /test/split_caml_module.t/specs.txt: -------------------------------------------------------------------------------- 1 | val create : name:string -> unit -> t option 2 | [@@py_fun_name __init__] 3 | 4 | val name : t -> string 5 | -------------------------------------------------------------------------------- /test/split_caml_module.t/thing.py: -------------------------------------------------------------------------------- 1 | class Thing: 2 | def __init__(self, name): 3 | self.name = name 4 | -------------------------------------------------------------------------------- /test/test_specs_file.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open! Lib 3 | 4 | let with_data_as_file ~data ~f = 5 | let open Stdio in 6 | let fname, oc = Caml.Filename.open_temp_file "pyml_bindeng_test" "" in 7 | Exn.protectx 8 | ~f:(fun oc -> 9 | Out_channel.output_string oc data; 10 | Out_channel.flush oc; 11 | f ~file_name:fname) 12 | ~finally:Out_channel.close oc 13 | 14 | let test_data = 15 | {|val add : x:int -> y:int -> 16 | unit -> int 17 | 18 | # .......... 19 | # ................ 20 | 21 | # @py_fun_name=add 22 | val add_int : x:int -> 23 | y:int -> 24 | unit -> int 25 | [@@py_fun_name add] 26 | 27 | 28 | # I like cheese. 29 | # 30 | #And#Apples 31 | #Pie 32 | # 33 | # @py_fun_name=add 34 | val add_float : 35 | x:float -> y:float -> unit -> float 36 | [@@py_fun_name add] 37 | 38 | 39 | val sub : x:int -> y:int -> unit -> int 40 | [@@py_fun_name silly] 41 | [@@yummy cake_stuff] 42 | [@@thing what] 43 | 44 | # Comment at the end 45 | 46 | # HEHE 47 | |} 48 | 49 | let%expect_test _ = 50 | let specs = 51 | with_data_as_file ~data:test_data ~f:(fun ~file_name -> 52 | Specs_file.read file_name) 53 | in 54 | Stdio.print_s @@ [%sexp_of: Specs_file.spec list] specs; 55 | [%expect 56 | {| 57 | (((attrs ()) (val_spec "val add : x:int -> y:int -> unit -> int")) 58 | ((attrs ("[@@py_fun_name add]")) 59 | (val_spec "val add_int : x:int -> y:int -> unit -> int")) 60 | ((attrs ("[@@py_fun_name add]")) 61 | (val_spec "val add_float : x:float -> y:float -> unit -> float")) 62 | ((attrs ("[@@py_fun_name silly] [@@yummy cake_stuff] [@@thing what]")) 63 | (val_spec "val sub : x:int -> y:int -> unit -> int"))) |}] 64 | 65 | let%expect_test "attributes must start a line" = 66 | let data = {| val f : int [@@apple pie] -> int |} in 67 | let specs = 68 | with_data_as_file ~data ~f:(fun ~file_name -> 69 | Or_error.try_with (fun () -> Specs_file.read file_name)) 70 | in 71 | Stdio.print_s @@ [%sexp_of: Specs_file.spec list Or_error.t] specs; 72 | [%expect {| (Error (Failure "attributes must start a line")) |}] 73 | 74 | let%expect_test _ = 75 | let data = {| 76 | int -> int 77 | [@@apple pie] 78 | [@@is good] 79 | |} in 80 | let specs = 81 | with_data_as_file ~data ~f:(fun ~file_name -> 82 | Or_error.try_with (fun () -> Specs_file.read file_name)) 83 | in 84 | Stdio.print_s @@ [%sexp_of: Specs_file.spec list Or_error.t] specs; 85 | [%expect 86 | {| (Error (Failure "In the middle of a val spec, but have none to work on.")) |}] 87 | 88 | let%expect_test _ = 89 | let data = {| 90 | [@@apple pie] 91 | [@@is good] 92 | |} in 93 | let specs = 94 | with_data_as_file ~data ~f:(fun ~file_name -> 95 | Or_error.try_with (fun () -> Specs_file.read file_name)) 96 | in 97 | Stdio.print_s @@ [%sexp_of: Specs_file.spec list Or_error.t] specs; 98 | [%expect 99 | {| (Error (Failure "We have attributes but no val spec for them to go with.")) |}] 100 | 101 | let%expect_test _ = 102 | let data = {| 103 | val f : int 104 | [@@apple pie] 105 | int -> float 106 | |} in 107 | let specs = 108 | with_data_as_file ~data ~f:(fun ~file_name -> 109 | Or_error.try_with (fun () -> Specs_file.read file_name)) 110 | in 111 | Stdio.print_s @@ [%sexp_of: Specs_file.spec list Or_error.t] specs; 112 | [%expect 113 | {| (Error (Failure "Found unused attrs but in the middle of a val spec.")) |}] 114 | 115 | let%expect_test _ = 116 | let data = {| 117 | val f : int -> 118 | float 119 | |} in 120 | let specs = 121 | with_data_as_file ~data ~f:(fun ~file_name -> 122 | Or_error.try_with (fun () -> Specs_file.read file_name)) 123 | in 124 | Stdio.print_s @@ [%sexp_of: Specs_file.spec list Or_error.t] specs; 125 | [%expect {| (Ok (((attrs ()) (val_spec "val f : int -> float")))) |}] 126 | 127 | let%expect_test _ = 128 | let data = {| 129 | val f : int -> 130 | float 131 | [@@hello world]|} in 132 | let specs = 133 | with_data_as_file ~data ~f:(fun ~file_name -> 134 | Or_error.try_with (fun () -> Specs_file.read file_name)) 135 | in 136 | Stdio.print_s @@ [%sexp_of: Specs_file.spec list Or_error.t] specs; 137 | [%expect 138 | {| (Ok (((attrs ("[@@hello world]")) (val_spec "val f : int -> float")))) |}] 139 | 140 | let%expect_test _ = 141 | let data = {| 142 | val f : int -> float 143 | [@@hello world] 144 | |} in 145 | let specs = 146 | with_data_as_file ~data ~f:(fun ~file_name -> 147 | Or_error.try_with (fun () -> Specs_file.read file_name)) 148 | in 149 | Stdio.print_s @@ [%sexp_of: Specs_file.spec list Or_error.t] specs; 150 | [%expect 151 | {| (Ok (((attrs ("[@@hello world]")) (val_spec "val f : int -> float")))) |}] 152 | 153 | let%expect_test _ = 154 | let data = {| val f : int -> float |} in 155 | let specs = 156 | with_data_as_file ~data ~f:(fun ~file_name -> 157 | Or_error.try_with (fun () -> Specs_file.read file_name)) 158 | in 159 | Stdio.print_s @@ [%sexp_of: Specs_file.spec list Or_error.t] specs; 160 | [%expect {| (Ok (((attrs ()) (val_spec "val f : int -> float")))) |}] 161 | -------------------------------------------------------------------------------- /test_dev/README.md: -------------------------------------------------------------------------------- 1 | # Development tests 2 | 3 | These tests are only run in the `dev` environment. 4 | 5 | Mainly they include tests in which any part of Cmdliner's output is present. See this [Discuss post](https://discuss.ocaml.org/t/ann-cmdliner-1-1-0/9295/2) for more info. 6 | -------------------------------------------------------------------------------- /test_dev/combine_rec_modules_cli_error.t: -------------------------------------------------------------------------------- 1 | Errors 2 | 3 | $ combine_rec_modules 4 | combine_rec_modules: required argument FILE is missing 5 | Usage: combine_rec_modules [OPTION]… FILE… 6 | Try 'combine_rec_modules --help' for more information. 7 | [1] 8 | $ combine_rec_modules missing.ml 9 | ERROR -- File missing.ml does not exist 10 | [1] 11 | $ combine_rec_modules missing.ml missing2.ml 12 | ERROR -- These files do not exist: missing.ml, missing2.ml 13 | [1] 14 | 15 | -------------------------------------------------------------------------------- /test_dev/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (enabled_if 3 | (= %{profile} dev)) 4 | (deps %{bin:combine_rec_modules} %{bin:gen_multi} %{bin:pyml_bindgen})) 5 | -------------------------------------------------------------------------------- /test_dev/gen_multi_cli_error.t/bad.tsv: -------------------------------------------------------------------------------- 1 | apple pie 2 | is good -------------------------------------------------------------------------------- /test_dev/gen_multi_cli_error.t/run.t: -------------------------------------------------------------------------------- 1 | Relative filenames will be relative to where the command is run, not where the 2 | file is. 3 | 4 | $ gen_multi specs/bad/bad.tsv 5 | (Sys_error "../cat_spec.txt: No such file or directory") 6 | [1] 7 | 8 | Errors 9 | 10 | $ gen_multi 11 | gen_multi: required argument CLI_OPTS is missing 12 | Usage: gen_multi [OPTION]… CLI_OPTS 13 | Try 'gen_multi --help' for more information. 14 | [1] 15 | $ gen_multi fake_file 16 | gen_multi: CLI_OPTS argument: no 'fake_file' file 17 | Usage: gen_multi [OPTION]… CLI_OPTS 18 | Try 'gen_multi --help' for more information. 19 | [1] 20 | $ gen_multi bad.tsv 21 | (Failure "bad config line: is good") 22 | [1] 23 | 24 | It tells you about all the files that are bad. 25 | 26 | $ gen_multi specs/bad_file_names.tsv 27 | ((Sys_error "specs/human_spec.txttt: No such file or directory") 28 | (Sys_error "specs/cat_spec.txttt: No such file or directory")) 29 | [1] 30 | -------------------------------------------------------------------------------- /test_dev/gen_multi_cli_error.t/specs/bad/bad.tsv: -------------------------------------------------------------------------------- 1 | signatures py_module py_class associated_with caml_module split_caml_module embed_python_source of_pyo_ret_type 2 | ../cat_spec.txt cat Cat class Cat NA NA no_check 3 | -------------------------------------------------------------------------------- /test_dev/gen_multi_cli_error.t/specs/bad_file_names.tsv: -------------------------------------------------------------------------------- 1 | signatures py_module py_class associated_with caml_module split_caml_module embed_python_source of_pyo_ret_type 2 | specs/human_spec.txttt human Human class Human NA NA no_check 3 | specs/cat_spec.txttt cat Cat class Cat NA NA no_check 4 | -------------------------------------------------------------------------------- /test_dev/pyml_bindgen_cli_error.t/run.t: -------------------------------------------------------------------------------- 1 | No arguments. 2 | 3 | $ pyml_bindgen 4 | pyml_bindgen: required arguments SIGNATURES, PY_MODULE, PY_CLASS are missing 5 | Usage: pyml_bindgen [OPTION]… SIGNATURES PY_MODULE PY_CLASS 6 | Try 'pyml_bindgen --help' for more information. 7 | [1] 8 | 9 | Version screen 10 | 11 | $ pyml_bindgen --version 12 | 0.4.0-SNAPSHOT 13 | 14 | Help screen 15 | 16 | $ pyml_bindgen --help=plain 17 | NAME 18 | pyml_bindgen - generate pyml bindings for a set of signatures 19 | 20 | SYNOPSIS 21 | pyml_bindgen [OPTION]… SIGNATURES PY_MODULE PY_CLASS 22 | 23 | DESCRIPTION 24 | Generate pyml bindings from OCaml signatures. 25 | 26 | ARGUMENTS 27 | PY_CLASS (required) 28 | Python class name 29 | 30 | PY_MODULE (required) 31 | Python module name 32 | 33 | SIGNATURES (required) 34 | Path to signatures 35 | 36 | OPTIONS 37 | -a ASSOCIATED_WITH, --associated-with=ASSOCIATED_WITH (absent=class) 38 | Are the Python functions associated with a class or just a module? 39 | ASSOCIATED_WITH must be either class or module. 40 | 41 | -c CAML_MODULE, --caml-module=CAML_MODULE 42 | Write full module and signature 43 | 44 | -e PYTHON_SOURCE, --embed-python-source=PYTHON_SOURCE 45 | Use this option to embed Python source code directly in the OCaml 46 | binary. In this way, you won't have to ensure the Python 47 | interpreter can find the module at runtime. 48 | 49 | -r OF_PYO_RET_TYPE, --of-pyo-ret-type=OF_PYO_RET_TYPE (absent=option) 50 | Return type of the of_pyobject function. OF_PYO_RET_TYPE must be 51 | one of no_check, option or or_error. 52 | 53 | -s SPLIT_CAML_MODULE, --split-caml-module=SPLIT_CAML_MODULE 54 | Split sig and impl into .ml and .mli files. Puts results in the 55 | specified dir. Dir is created if it does not exist. 56 | 57 | COMMON OPTIONS 58 | --help[=FMT] (default=auto) 59 | Show this help in format FMT. The value FMT must be one of auto, 60 | pager, groff or plain. With auto, the format is pager or plain 61 | whenever the TERM env var is dumb or undefined. 62 | 63 | --version 64 | Show version information. 65 | 66 | BUGS 67 | Please report any bugs or issues on GitHub. 68 | (https://github.com/mooreryan/pyml_bindgen/issues) 69 | 70 | SEE ALSO 71 | For full documentation, please see the GitHub page. 72 | (https://github.com/mooreryan/pyml_bindgen) 73 | 74 | AUTHORS 75 | Ryan M. Moore 76 | 77 | File doesn't exist. 78 | 79 | $ pyml_bindgen apple pie good 80 | pyml_bindgen: SIGNATURES argument: no 'apple' file 81 | Usage: pyml_bindgen [OPTION]… SIGNATURES PY_MODULE PY_CLASS 82 | Try 'pyml_bindgen --help' for more information. 83 | [1] 84 | 85 | Passing `split-caml-module` without `caml-module`. 86 | 87 | $ pyml_bindgen specs.txt silly Silly --split-caml-module abc 88 | ERROR: --split-caml-module was given but --caml-module was not 89 | [1] 90 | 91 | No value for --split-caml-module 92 | 93 | $ pyml_bindgen specs.txt silly Silly --caml-module Silly --split-caml-module 94 | pyml_bindgen: option '--split-caml-module' needs an argument 95 | Usage: pyml_bindgen [OPTION]… SIGNATURES PY_MODULE PY_CLASS 96 | Try 'pyml_bindgen --help' for more information. 97 | [1] 98 | 99 | -------------------------------------------------------------------------------- /test_dev/pyml_bindgen_cli_error.t/specs.txt: -------------------------------------------------------------------------------- 1 | val foo : t -> unit -> string 2 | --------------------------------------------------------------------------------